docs: add comprehensive test behavior documentation for all pages
Add behavior documentation covering all SSR and legacy SPA pages: - Testing strategy and type definitions (unit/integration/UI) - Dashboard, Invoice, Payment, Transaction, Ledger pages - Company/Settings, POS, Admin, Search, Auth pages - Legacy SPA behavior docs (no UI tests until migrated) - Edge cases, test data requirements, and dependencies per subsystem Total: 3,600+ lines of behavior documentation to guide test authorship.
This commit is contained in:
185
docs/testing/README.md
Normal file
185
docs/testing/README.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# Integreat Test Strategy & Behavior Documentation
|
||||
|
||||
**Last Updated:** 2026-05-04
|
||||
|
||||
## Purpose
|
||||
|
||||
This document defines the comprehensive testing strategy for Integreat. The goal is to cover all user-visible behaviors with tests that provide confidence in the system's correctness. We are preserving valuable existing tests and filling gaps with new behavior documentation.
|
||||
|
||||
## Test Types
|
||||
|
||||
### 1. Unit Tests
|
||||
- **Scope:** Pure functions, transformations, validations, business logic
|
||||
- **Characteristics:** No database, no external services, deterministic
|
||||
- **Location:** `test/clj/auto_ap/<namespace>_test.clj`
|
||||
- **Examples:** `new_invoice_wizard_test.clj` (location spreading logic)
|
||||
|
||||
### 2. Integration Tests
|
||||
- **Scope:** Database interactions, GraphQL resolvers, route handlers, cross-system behavior
|
||||
- **Characteristics:** Uses Datomic in-memory database (`datomic:mem://test`), schema created per test, data cleaned up after
|
||||
- **Location:** `test/clj/auto_ap/integration/`
|
||||
- **Fixtures:** `wrap-setup` creates schema + functions, runs test, deletes DB
|
||||
- **Helpers:** `test/clj/auto_ap/integration/util.clj` provides entity creation helpers
|
||||
|
||||
### 3. UI / Functional Tests (Playwright)
|
||||
- **Scope:** End-to-end user flows, page navigation, form interactions, HTMX behaviors
|
||||
- **Characteristics:** Runs against running application, exercises real HTTP routes
|
||||
- **Location:** TBD (likely `test/functional/` or similar)
|
||||
- **Scope Limitation:** Only SSR pages (HTMX-based) get UI tests. Legacy SPA pages get behavior docs only.
|
||||
|
||||
## Existing Test Inventory
|
||||
|
||||
### Unit Tests
|
||||
| File | Coverage | Status |
|
||||
|------|----------|--------|
|
||||
| `auto_ap/ezcater_test.clj` | EzCater integration logic | Keep |
|
||||
| `auto_ap/import/plaid_test.clj` | Plaid import | Keep |
|
||||
| `auto_ap/import/transactions_test.clj` | Transaction import | Keep |
|
||||
| `auto_ap/import/yodlee_test.clj` | Yodlee import | Keep |
|
||||
| `auto_ap/import/manual_test.clj` | Manual import | Keep |
|
||||
| `auto_ap/ledger_test.clj` | Ledger calculations | Keep |
|
||||
| `auto_ap/parse/templates_test.clj` | PDF template parsing | Keep |
|
||||
| `auto_ap/ssr/invoice/new_invoice_wizard_test.clj` | Location spreading logic | Keep |
|
||||
|
||||
### Integration Tests
|
||||
| File | Coverage | Status |
|
||||
|------|----------|--------|
|
||||
| `auto_ap/integration/graphql.clj` | Transaction page, invoice page, ledger page, vendors, transaction rules | Keep |
|
||||
| `auto_ap/integration/graphql/accounts.clj` | Account GraphQL | Keep |
|
||||
| `auto_ap/integration/graphql/checks.clj` | Check GraphQL | Keep |
|
||||
| `auto_ap/integration/graphql/invoices.clj` | Invoice GraphQL | Keep |
|
||||
| `auto_ap/integration/graphql/ledger/running_balance.clj` | Ledger balance | Keep |
|
||||
| `auto_ap/integration/graphql/transaction_rules.clj` | Transaction rules | Keep |
|
||||
| `auto_ap/integration/graphql/transactions.clj` | Transaction GraphQL | Keep |
|
||||
| `auto_ap/integration/graphql/users.clj` | User GraphQL | Keep |
|
||||
| `auto_ap/integration/graphql/vendors.clj` | Vendor GraphQL | Keep |
|
||||
| `auto_ap/integration/routes/invoice_test.clj` | Invoice import routes | Keep |
|
||||
| `auto_ap/integration/routes/ezcater_xls.clj` | EzCater XLS routes | Keep |
|
||||
| `auto_ap/integration/rule_matching.clj` | Rule matching logic | Keep |
|
||||
| `auto_ap/integration/jobs/ntg.clj` | NTG background job | Keep |
|
||||
|
||||
## Page/Subsystem Coverage Map
|
||||
|
||||
### SSR Pages (HTMX - Eligible for UI Tests)
|
||||
1. **Dashboard** - Main dashboard with cards (expenses, tasks, bank accounts, sales, P&L)
|
||||
2. **Invoices** - List, detail, new wizard, pay wizard, bulk edit, bulk delete, import, glimpse (OCR)
|
||||
3. **Payments** - List, detail, bulk operations
|
||||
4. **Transactions** - List, detail, new, external import, coding workflow
|
||||
5. **Ledger** - Entries, P&L, Balance Sheet, Cash Flows, investigation
|
||||
6. **Company** - Profile, signature, 1099s, reports, bank accounts, Plaid/Yodlee linking
|
||||
7. **POS** - Sales, expected deposits, tenders, refunds, cash drawer shifts
|
||||
8. **Outgoing Invoices** - Create outgoing invoices
|
||||
9. **Admin** - Clients, accounts, vendors, transaction rules, background jobs, history, import batches, Excel invoices, sales summaries
|
||||
10. **Search** - Global search dialog
|
||||
11. **Indicators** - Business indicators
|
||||
|
||||
### Legacy SPA Pages (Behavior Docs Only)
|
||||
1. **Home** - Legacy dashboard
|
||||
2. **Login** - Authentication
|
||||
3. **Transactions** - Unapproved, approved, requires feedback, excluded
|
||||
4. **Ledger** - P&L, Balance Sheet, Cash Flows, external, external import
|
||||
5. **Payments** - Legacy payments list
|
||||
6. **Reports** - Reports page
|
||||
7. **Admin Vendors** - Vendor management (legacy)
|
||||
8. **New Vendor** - Vendor creation
|
||||
|
||||
## Behavior Documentation Structure
|
||||
|
||||
Each subsystem gets a markdown file in `docs/testing/behaviors/` with the following structure:
|
||||
|
||||
```markdown
|
||||
# <Subsystem> Behaviors
|
||||
|
||||
## Overview
|
||||
Brief description of what this subsystem does and its user-visible purpose.
|
||||
|
||||
## Pages / Routes
|
||||
List of all routes and their purposes.
|
||||
|
||||
## Behaviors by Type
|
||||
|
||||
### Unit Test Behaviors
|
||||
- Pure function behaviors, edge cases
|
||||
|
||||
### Integration Test Behaviors
|
||||
- Database interactions, API behaviors, cross-system flows
|
||||
|
||||
### UI Test Behaviors (SSR only)
|
||||
- End-to-end happy paths
|
||||
- User interaction flows
|
||||
- Navigation between pages
|
||||
|
||||
## Edge Cases
|
||||
- Error states
|
||||
- Empty states
|
||||
- Permission boundaries
|
||||
- Concurrent operations
|
||||
- Large data volumes
|
||||
|
||||
## Test Data Requirements
|
||||
What entities need to exist for meaningful tests.
|
||||
|
||||
## Dependencies
|
||||
What other subsystems this depends on.
|
||||
```
|
||||
|
||||
## Test Data Patterns
|
||||
|
||||
The integration test utilities in `test/clj/auto_ap/integration/util.clj` provide:
|
||||
- `test-client` - Creates a test client entity
|
||||
- `test-vendor` - Creates a test vendor entity
|
||||
- `test-bank-account` - Creates a test bank account
|
||||
- `test-transaction` - Creates a test transaction
|
||||
- `test-payment` - Creates a test payment
|
||||
- `test-invoice` - Creates a test invoice
|
||||
- `test-account` - Creates a test GL account
|
||||
- `test-transaction-rule` - Creates a test transaction rule
|
||||
- `setup-test-data` - Convenience function to create standard test data set
|
||||
- `admin-token` / `user-token` - JWT identity helpers
|
||||
|
||||
## Mocks & External Services
|
||||
|
||||
- **Solr:** Mocked for search functionality
|
||||
- **AWS Services:** (Textract, S3, SES, SQS) - Should be mocked in tests
|
||||
- **Plaid/Yodlee/Intuit:** External financial APIs - Mocked
|
||||
- **Square/EzCater:** POS integrations - Mocked
|
||||
|
||||
## Priorities
|
||||
|
||||
1. **Critical:** Invoice pages (core business function)
|
||||
2. **Critical:** Payment pages (money movement)
|
||||
3. **High:** Dashboard (first thing users see)
|
||||
4. **High:** Transaction pages (data entry/coding)
|
||||
5. **High:** Ledger reports (financial reporting)
|
||||
6. **Medium:** Admin pages (operations)
|
||||
7. **Medium:** Company settings (configuration)
|
||||
8. **Low:** POS pages (ancillary)
|
||||
9. **Low:** Legacy SPA pages (behavior docs only, no UI tests)
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
# All tests
|
||||
lein test
|
||||
|
||||
# Integration tests only
|
||||
lein test :integration
|
||||
|
||||
# Functional tests only
|
||||
lein test :functional
|
||||
```
|
||||
|
||||
## Files
|
||||
|
||||
- [Dashboard Behaviors](behaviors/dashboard.md)
|
||||
- [Invoice Behaviors](behaviors/invoice.md)
|
||||
- [Payment Behaviors](behaviors/payment.md)
|
||||
- [Transaction Behaviors](behaviors/transaction.md)
|
||||
- [Ledger Behaviors](behaviors/ledger.md)
|
||||
- [Company Behaviors](behaviors/company.md)
|
||||
- [POS Behaviors](behaviors/pos.md)
|
||||
- [Outgoing Invoice Behaviors](behaviors/outgoing-invoice.md)
|
||||
- [Admin Behaviors](behaviors/admin.md)
|
||||
- [Search & Indicators Behaviors](behaviors/search-indicators.md)
|
||||
- [Auth Behaviors](behaviors/auth.md)
|
||||
- [Legacy SPA Behaviors](behaviors/legacy-spa.md)
|
||||
397
docs/testing/behaviors/admin.md
Normal file
397
docs/testing/behaviors/admin.md
Normal file
@@ -0,0 +1,397 @@
|
||||
# Admin Behaviors
|
||||
|
||||
## Overview
|
||||
|
||||
The Admin section is a server-side rendered (SSR) HTMX interface for superuser operations in Integreat. All admin pages require the `admin` user role. Non-admin users are redirected away. The admin area includes dashboards, entity management grids, background job orchestration, audit history, and bulk import tools.
|
||||
|
||||
## Routes & Pages
|
||||
|
||||
| Page | Route | Handler Namespace |
|
||||
|------|-------|-------------------|
|
||||
| Admin Dashboard | `/admin` | `auto-ap.ssr.admin` |
|
||||
| Clients | `/admin/clients` | `auto-ap.ssr.admin.clients` |
|
||||
| Accounts | `/admin/accounts` | `auto-ap.ssr.admin.accounts` |
|
||||
| Vendors | `/admin/vendors` | `auto-ap.ssr.admin.vendors` |
|
||||
| Transaction Rules | `/admin/transaction-rules` | `auto-ap.ssr.admin.transaction-rules` |
|
||||
| Background Jobs | `/admin/jobs` | `auto-ap.ssr.admin.background-jobs` |
|
||||
| History / Audit | `/admin/history` | `auto-ap.ssr.admin.history` |
|
||||
| Import Batches | `/admin/import-batches` | `auto-ap.ssr.admin.import-batch` |
|
||||
| Excel Invoices | `/admin/excel-invoices` | `auto-ap.ssr.admin.excel-invoice` |
|
||||
| Sales Summaries | `/admin/sales-summaries` | `auto-ap.ssr.admin.sales-summaries` |
|
||||
|
||||
All routes are wrapped with `wrap-client-redirect-unauthenticated` followed by `wrap-admin`.
|
||||
|
||||
## Behaviors by Page
|
||||
|
||||
### Admin Dashboard
|
||||
|
||||
**Display**
|
||||
- Shows two Chartist charts:
|
||||
- **Growth in clients**: Bar chart showing client count at 2 years ago, 1 year ago, and today (uses `dc/as-of` against Datomic history).
|
||||
- **Changes by hour**: Line chart showing Datomic transaction counts per hour over the last 24 hours.
|
||||
- Uses the standard admin page layout with `admin-aside-nav`.
|
||||
|
||||
**Access Control**
|
||||
- Only accessible to users with admin role.
|
||||
- Unauthenticated users are redirected to login.
|
||||
|
||||
### Client Management
|
||||
|
||||
**Grid Display**
|
||||
- Columns: Name, Code, Locations (as pills), Emails (as pills), Status.
|
||||
- Status column shows:
|
||||
- Lock status: "Locked <date>" with color coding (green < 90 days, yellow < 365 days, red otherwise).
|
||||
- "Not locked" in red if no lock date.
|
||||
- Bank account integration status pills (red for failed/unauthorized).
|
||||
- Row actions: Biweekly Sales PowerQuery button, Edit button (pencil icon).
|
||||
- Global action: "New Client" button opens wizard.
|
||||
|
||||
**Filtering**
|
||||
- Name (case-insensitive substring match).
|
||||
- Code (exact match, upper-cased).
|
||||
- Group (exact match on client groups, upper-cased).
|
||||
- Select filter: "All" or "Only mine" (filters to clients assigned to current user).
|
||||
- Filters trigger HTMX request with 500ms debounce on change, 1000ms on keyup.
|
||||
|
||||
**Sorting**
|
||||
- By name, code.
|
||||
- Pagination: 25 per page default.
|
||||
|
||||
**Client Wizard (Create / Edit)**
|
||||
- Multi-step linear wizard with steps: Info → Matches → Contact → Bank Accounts → Integrations → Cash Flow → Other Settings.
|
||||
- **Info step**: Name (required), Code (immutable on edit), Locked Until date, Locations (dynamic grid).
|
||||
- **Matches step**: String match patterns, location match patterns (match → location mapping).
|
||||
- **Contact step**: Address (street1, street2, city, state, zip), Email contacts grid (description + email).
|
||||
- **Bank Accounts step**: Sortable card list of existing accounts. Add new cash/credit/checking accounts. Each account has type-specific form.
|
||||
- Cash account: nickname, code, financial code (numeric), start date, include-in-reports, visible-for-payment.
|
||||
- Credit card: same + bank name, account number, Plaid/Yodlee/Intuit integration selectors.
|
||||
- Checking: same + routing number, bank code, check number.
|
||||
- Validation: financial code is required if "Include in Reports" is true.
|
||||
- **Integrations step**: Square auth token input, refresh Square locations button, Square location → client location mapping grid.
|
||||
- **Cash Flow step**: Week A/B credits and debits (4 numeric fields).
|
||||
- **Other Settings step**: Feature flags (dropdown with known flags), Groups (free text grid).
|
||||
|
||||
**Save Behavior**
|
||||
- POST for create, PUT for update.
|
||||
- Code uniqueness validation on create.
|
||||
- Groups are upper-cased on save.
|
||||
- Bank account start dates coerced to dates.
|
||||
- After save: row flashes in grid, modal closes, Solr reindexes client.
|
||||
|
||||
**Biweekly Sales PowerQuery**
|
||||
- Generates 6 saved queries per client (sales summary, category, expected deposits, tenders, refunds, cash drawer shifts).
|
||||
- Opens modal with copy-to-clipboard buttons for Excel Power Query M-code.
|
||||
|
||||
### Account Management
|
||||
|
||||
**Grid Display**
|
||||
- Columns: Code (numeric), Name, Type (pill), Location.
|
||||
- Row action: Edit button.
|
||||
- Global action: "New Account" button.
|
||||
|
||||
**Filtering**
|
||||
- Name (case-insensitive substring on upper-cased name).
|
||||
- Code (exact numeric match).
|
||||
- Type: All, Dividend, Asset, Equity, Liability, Expense, Revenue, None.
|
||||
|
||||
**Sorting**
|
||||
- By code, name, type, location.
|
||||
- Default sort by upper-cased numeric code.
|
||||
|
||||
**Account Dialog (Create / Edit)**
|
||||
- Modal card with live-updating header showing code and name.
|
||||
- Fields:
|
||||
- Numeric Code (required, unique on create; hidden on edit).
|
||||
- Name (required).
|
||||
- Account Type (required, enum: asset, liability, equity, revenue, expense, dividend, none).
|
||||
- Location (optional string).
|
||||
- Invoice Allowance (enum: allowed, denied, warn).
|
||||
- Vendor Allowance (enum: allowed, denied, warn).
|
||||
- Applicability (enum: global, customized).
|
||||
- Client Overrides grid: client typeahead + override name. Validates no duplicate clients.
|
||||
|
||||
**Save Behavior**
|
||||
- POST for create, PUT for update.
|
||||
- Numeric code uniqueness check on create.
|
||||
- Client override uniqueness validation.
|
||||
- After save: Solr reindexes account + all client overrides.
|
||||
|
||||
### Vendor Management
|
||||
|
||||
**Grid Display**
|
||||
- Columns: Name (with usage pills), Email, Default Account.
|
||||
- Usage pills: "Unused" (red) if 0 clients, "Used by N clients" (primary), "Used N times" (secondary).
|
||||
- Row action: Edit button (opens wizard).
|
||||
- Global actions: "Merge" button, "New Vendor" button.
|
||||
|
||||
**Filtering**
|
||||
- Name (case-insensitive substring on upper-cased name).
|
||||
- Type: All, Only hidden, Only global.
|
||||
|
||||
**Vendor Wizard (Create / Edit)**
|
||||
- Timeline steps: Info → Terms → Account → Address → Legal.
|
||||
- **Info step**: Name (required, 3+ chars), Print As (optional, toggleable), Hidden (admin-only checkbox).
|
||||
- **Terms step**: Terms in days (integer), Terms Overrides grid (client + days), Automatically Paid When Due grid (client list).
|
||||
- **Account step**: Default Account (typeahead), Account Overrides grid (client + account typeahead). Account typeahead is scoped by selected client.
|
||||
- **Address step**: Street1, Street2, City, State (2 chars), Zip (5 chars).
|
||||
- **Legal step**: Legal Entity Name OR First/Middle/Last name, TIN, TIN Type (EIN/SSN), 1099 Type (enum).
|
||||
|
||||
**Vendor Merge**
|
||||
- Modal with Source Vendor (to be deleted) and Target Vendor.
|
||||
- Validation: source and target must differ.
|
||||
- On merge: all references to source vendor are retracted and asserted as target vendor. Source vendor entity is retracted.
|
||||
- Shows success notification after merge.
|
||||
|
||||
**Save Behavior**
|
||||
- POST for create, PUT for update.
|
||||
- Solr reindexes vendor name + hidden flag.
|
||||
|
||||
### Transaction Rules
|
||||
|
||||
**Grid Display**
|
||||
- Columns: Client (or group pill), Bank Account, Description, Amount (gte/lte pills), Note.
|
||||
- Row actions: Delete (with confirmation), Execute (play icon), Edit (pencil icon).
|
||||
- Global action: "New Transaction Rule" button.
|
||||
|
||||
**Filtering**
|
||||
- Vendor (entity typeahead).
|
||||
- Note (case-insensitive regex match).
|
||||
- Description (case-insensitive substring).
|
||||
- Client Group (exact upper-cased match).
|
||||
|
||||
**Transaction Rule Wizard**
|
||||
- Two-step wizard: Edit → Test.
|
||||
- **Edit step**:
|
||||
- Description (required, regex pattern, 3+ chars).
|
||||
- Optional togglable filters: Client, Client Group, Bank Account, Amount range (gte/lte), Day of Month range (gte/lte).
|
||||
- Bank account selector is scoped by selected client.
|
||||
- Outcomes: Assign Vendor (typeahead), Accounts grid (account + location + percentage), Approval Status (radio: unapproved/approved).
|
||||
- Account location is derived from account's fixed location, client locations, or "Shared".
|
||||
- Validation: account percentages must sum to 100%, bank account must belong to selected client, location must match account's fixed location if set.
|
||||
- **Test step**: Shows matching transactions (up to 15) with client, bank, date, description. Badge shows total count ("99+" if ≥99).
|
||||
|
||||
**Execute**
|
||||
- Opens dialog with checkbox-selectable transactions that match the rule AND are unapproved.
|
||||
- Only transactions on or after client's `locked-until` date are included.
|
||||
- Can select "All" or individual transactions.
|
||||
- On execution: applies rule coding to each transaction via `upsert-transaction`, touches Solr index.
|
||||
- Shows notification: "Successfully coded X of Y transactions!"
|
||||
|
||||
**Delete**
|
||||
- Confirms before delete.
|
||||
- Retracts entity from Datomic.
|
||||
- Row fades out with "live-removed" class.
|
||||
|
||||
### Background Jobs
|
||||
|
||||
**Grid Display**
|
||||
- Columns: Start time, End time, Duration (minutes), Name, Status.
|
||||
- Status values: `:running`, `:pending`, `:succeeded`, `:failed`.
|
||||
- Data source: ECS tasks filtered by `INTEGREAT_JOB` environment variable.
|
||||
- Global action: "Run job" button.
|
||||
|
||||
**Job Start Dialog**
|
||||
- Job type dropdown: Yodlee Import, Yodlee Account Import, Intuit Import, Plaid Import, Bulk Journal Import, Square Import, Register Invoice Import, Upsert recent ezcater orders, Load Historical Square Sales, Export Backup.
|
||||
- Dynamic subform based on job type:
|
||||
- Bulk Journal Import: S3 URL path input.
|
||||
- Register Invoice Import: S3 URL path input.
|
||||
- Load Historical Sales: Client typeahead + Days (1-120).
|
||||
- Validation: cannot start a job that is already running.
|
||||
- On submit: runs ECS Fargate Spot task with selected task definition.
|
||||
|
||||
### History / Audit
|
||||
|
||||
**Display**
|
||||
- Search box for entity ID (numeric Datomic entity ID).
|
||||
- History table shows: Date, User, Field, From value, To value.
|
||||
- Values are formatted: dates → local format, large integers (>1M) → clickable links to that entity's history, nil → "(none)", others → pr-str.
|
||||
- Entity ID links load that entity's history inline.
|
||||
- Snapshot link opens inspector showing full entity pull `[*]`.
|
||||
- No pagination (all history rows shown).
|
||||
|
||||
**Inspector**
|
||||
- Card showing all attributes of an entity at current database value.
|
||||
- Clickable entity IDs recurse into history.
|
||||
|
||||
### Import Batches
|
||||
|
||||
**Grid Display**
|
||||
- Columns: Date, Source, Status, User, Imported, Pre-existing, Suppressed.
|
||||
- Row action: External link to transactions filtered by import batch.
|
||||
|
||||
**Filtering**
|
||||
- Date range (start/end).
|
||||
- Source (enum: import-source values).
|
||||
|
||||
**Sorting**
|
||||
- By date, source, status, user.
|
||||
|
||||
### Excel Invoices
|
||||
|
||||
**Display**
|
||||
- Single-page form with large textarea for tab-separated invoice data.
|
||||
- Shows sample data as placeholder.
|
||||
|
||||
**Import Behavior**
|
||||
- Parses tab-separated rows with columns: raw-date, vendor-name, check, location, invoice-number, amount, client-name, bill-entered, bill-rejected, added-on, exported-on, account-numeric-code.
|
||||
- Validates and resolves: client by code or name, vendor by name, account by numeric code.
|
||||
- Groups rows into: new, existing (by vendor+client+invoice-number), errors.
|
||||
- For new rows: creates invoice, optionally payment (if "Cash" check), and cash transaction.
|
||||
- Cash invoices get status "paid" with zero outstanding balance.
|
||||
- Non-cash invoices get status "unpaid" with full outstanding balance.
|
||||
- Results shown as pills: imported count, extant count, vendors not found (hover tooltip), error grid.
|
||||
|
||||
### Sales Summaries
|
||||
|
||||
**Grid Display**
|
||||
- Columns: Client (hidden if only one client), Date, Debits (itemized list), Credits (itemized list).
|
||||
- Each item shows category, amount, and "missing account" pill in red if no account mapped.
|
||||
- Total row shows "Total: $X" with green pill if balanced, red if unbalanced.
|
||||
- Row action: Edit button.
|
||||
|
||||
**Filtering**
|
||||
- Date range (start/end).
|
||||
- Scoped to user's valid clients.
|
||||
|
||||
**Edit Wizard**
|
||||
- Shows all sales summary items in a grid: Category, Account, Debits, Credits.
|
||||
- Manual items: editable category, account typeahead, debit/credit inputs, removable.
|
||||
- Auto items: read-only category and amount, editable account.
|
||||
- Account typeahead is scoped by client and filtered for invoice-purpose accounts.
|
||||
- Live total row and unbalanced row update on amount changes.
|
||||
- Validation: each item must have exactly one of credit or debit, not both.
|
||||
- Validation: debits must equal credits (balanced).
|
||||
- Save updates ledger-mapped account assignments. Manual items get `manual?` flag and ledger-side/amount attached.
|
||||
|
||||
## Cross-Cutting Behaviors
|
||||
|
||||
### Admin-only Access
|
||||
- All admin routes use `wrap-admin` middleware.
|
||||
- Non-admin authenticated users receive an authorization failure (typically redirect or error).
|
||||
- Unauthenticated users are redirected to login via `wrap-client-redirect-unauthenticated`.
|
||||
|
||||
### Impersonation
|
||||
- Admin users can select a client from the global client selector.
|
||||
- Some admin grids (clients, transaction rules, sales summaries) respect the selected client and filter results accordingly.
|
||||
|
||||
### History Tracking
|
||||
- All mutating admin operations use `audit-transact` or `audit-transact-batch`.
|
||||
- Transactions record `:audit/user` attribute.
|
||||
- The History page queries Datomic's history database (`dc/history`) to show all changes to an entity.
|
||||
|
||||
### Permissions
|
||||
- Admin role is required for all admin handlers.
|
||||
- No finer-grained permissions within admin area.
|
||||
|
||||
### Form Validation Patterns
|
||||
- Malli schemas enforce form structure.
|
||||
- `wrap-schema-enforce` validates query params, route params, and form params.
|
||||
- `wrap-form-4xx-2` re-renders dialogs with validation errors on 400 responses.
|
||||
- HTMX response targets (`#form-errors .error-content`) display field-level errors.
|
||||
|
||||
### Solr Indexing
|
||||
- After create/update of clients, accounts, and vendors, Solr documents are reindexed.
|
||||
- This affects search typeaheads across the application.
|
||||
|
||||
## Edge Cases
|
||||
|
||||
**Client Management**
|
||||
- Client code must be unique on create; immutable on edit.
|
||||
- Bank account code is immutable after creation.
|
||||
- Financial code (numeric) required when "Include in Reports" is true.
|
||||
- Square location refresh times out after 2 seconds → shows "No locations found."
|
||||
- Bank accounts are sortable via drag-and-drop (sortable.js integration via `hx-trigger="end"`).
|
||||
|
||||
**Account Management**
|
||||
- Numeric code must be unique on create.
|
||||
- A client can only have one override per account.
|
||||
- Applicability and allowances are enums with specific values.
|
||||
|
||||
**Vendor Management**
|
||||
- Vendor merge fails if source equals target.
|
||||
- Account override typeahead depends on client selection; changing client updates available accounts.
|
||||
- Terms override requires unique clients (no duplicate client overrides).
|
||||
|
||||
**Transaction Rules**
|
||||
- Rule execution only affects unapproved transactions.
|
||||
- Transactions before client's `locked-until` date are excluded from execution.
|
||||
- Account percentages must sum to exactly 100%.
|
||||
- If account has a fixed location, the rule location must match.
|
||||
|
||||
**Background Jobs**
|
||||
- Cannot start a job that is already running (based on ECS task status).
|
||||
- ECS API failures propagate as exceptions.
|
||||
|
||||
**Excel Invoices**
|
||||
- Vendor name resolution is case-sensitive exact match.
|
||||
- Location format expected: `CLIENTCODE-LOCATION`.
|
||||
- Missing client code or location adds error row.
|
||||
- Cash check type creates paid invoice with transaction; others create unpaid invoice.
|
||||
|
||||
**Sales Summaries**
|
||||
- Debits and credits must balance to save.
|
||||
- Manual items require both category and amount.
|
||||
- Account typeahead is scoped to client + invoice-purpose accounts.
|
||||
|
||||
**History**
|
||||
- Entity ID must be parseable as a Long; otherwise shows error notification.
|
||||
- Very old entities may have no history if created before audit attributes.
|
||||
|
||||
## Test Data Requirements
|
||||
|
||||
### Users
|
||||
- Admin user with `:user/role :user-role/admin`.
|
||||
- Non-admin user with `:user/role :user-role/user` for access control tests.
|
||||
|
||||
### Clients
|
||||
- Client with `:client/name`, `:client/code` (unique), `:client/locations`.
|
||||
- Client with `:client/locked-until` in the past and future.
|
||||
- Client with bank accounts (cash, credit, checking types).
|
||||
- Client with Square auth token for integration tests.
|
||||
- Client with feature flags and groups.
|
||||
|
||||
### Accounts
|
||||
- Account with `:account/numeric-code` (unique), `:account/name`, `:account/type`.
|
||||
- Account with `:account/location` fixed.
|
||||
- Account with client overrides.
|
||||
|
||||
### Vendors
|
||||
- Vendor with `:vendor/name`, `:vendor/default-account`.
|
||||
- Vendor with terms overrides and account overrides.
|
||||
- Hidden and non-hidden vendors.
|
||||
- Vendor with 1099 legal entity info.
|
||||
|
||||
### Transaction Rules
|
||||
- Rule with description pattern, client, bank account, amount gte/lte.
|
||||
- Rule with account assignments summing to 100%.
|
||||
- Unapproved transactions matching rule criteria.
|
||||
- Transactions before and after client's locked-until date.
|
||||
|
||||
### Background Jobs
|
||||
- ECS cluster with task definitions containing `INTEGREAT_JOB` env var.
|
||||
- Or mock ECS responses for unit tests.
|
||||
|
||||
### Import Batches
|
||||
- Import batch entities with `:import-batch/date`, `:import-batch/source`, `:import-batch/status`.
|
||||
|
||||
### Sales Summaries
|
||||
- Sales summary with items having `:ledger-mapped/ledger-side` and `:ledger-mapped/amount`.
|
||||
- Both manual and auto-generated items.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Datomic (history, transactions, pull API)
|
||||
- HTMX (frontend interactivity)
|
||||
- Alpine.js (modal state, conditional visibility)
|
||||
- Chartist (dashboard charts)
|
||||
- Solr (search indexing)
|
||||
- ECS API (background jobs)
|
||||
- Malli (schema validation)
|
||||
- Bidi (routing)
|
||||
|
||||
## Related Test Files
|
||||
|
||||
- `test/clj/auto_ap/integration/graphql/users.clj` - User role and permission tests
|
||||
- `test/clj/auto_ap/integration/graphql/accounts.clj` - Account search and override tests
|
||||
- `test/clj/auto_ap/integration/graphql/vendors.clj` - Vendor management tests
|
||||
- `test/clj/auto_ap/integration/graphql/transaction_rules.clj` - Rule matching and execution tests
|
||||
166
docs/testing/behaviors/auth.md
Normal file
166
docs/testing/behaviors/auth.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# Authentication Behaviors
|
||||
|
||||
## Overview
|
||||
|
||||
Authentication in Integreat uses Google OAuth 2.0 as the primary identity provider. Users authenticate via Google, receive a JWT token, and establish a server-side session. The system supports role-based access control (`admin`, `user`, `read-only`), client-scoped permissions, session versioning for SSR rollout, and admin impersonation via signed JWT tokens.
|
||||
|
||||
## Routes & Pages
|
||||
|
||||
| Route | Method | Handler | Purpose |
|
||||
|-------|--------|---------|---------|
|
||||
| `/login` | GET | `:login` (SPA) | Legacy login page |
|
||||
| `/needs-activation` | GET | `:needs-activation` (SPA) | Account pending activation page |
|
||||
| `/api/oauth` | GET | `:oauth` | Google OAuth callback, creates/updates user, establishes session |
|
||||
| `/logout` | GET | `:logout` | Clears session, redirects to `/login` |
|
||||
| `/impersonate` | GET | `:impersonate` | Admin-only: assumes another user's identity via JWT |
|
||||
|
||||
## Behaviors
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- `user->jwt` returns nil when user or oauth-token is nil
|
||||
- `user->jwt` includes `:user`, `:exp` (30 days), `:db/id`, `:user/role`, `:user/name` for all users
|
||||
- `user->jwt` includes `:gz-clients` (gzip-compressed client list with code, id, locations) for admin and read-only users
|
||||
- `user->jwt` includes `:user/clients` (plain client list) for non-admin, non-read-only users
|
||||
- `user->jwt` excludes `:exp` from the returned JWT payload (expiration is handled by buddy.sign.jwt)
|
||||
- `gzip` compresses and base64-encodes a Clojure data structure
|
||||
- `gunzip` reverses `gzip`, restoring the original data structure
|
||||
- `make-api-token` creates a JWT with `:user "API"`, `:user/role "admin"`, `:user/name "API"`, expiring in 1000 days
|
||||
- `logout` returns 301 redirect to `/login` with empty session map
|
||||
- `impersonate` unsigns JWT from query param, dissoc's `:exp`, and returns 200 with new session
|
||||
- `impersonate` requires `wrap-secure` and `wrap-admin` middleware (enforced in route registration)
|
||||
- `wrap-secure` allows authenticated requests to proceed
|
||||
- `wrap-secure` returns 401 with `hx-redirect` to `/login` for HTMX requests without authentication
|
||||
- `wrap-secure` returns 302 redirect to `/login` with `redirect-to` param for normal requests without authentication
|
||||
- `wrap-admin` allows admin requests to proceed
|
||||
- `wrap-admin` returns 302 redirect to `/login` for non-admin requests
|
||||
- `wrap-client-redirect-unauthenticated` converts 401 responses to HTMX redirects for unauthenticated users
|
||||
- `wrap-session-version` invalidates sessions with mismatched version, redirecting to `/login`
|
||||
- `wrap-session-version` allows valid sessions to proceed
|
||||
- `wrap-session-version` returns 401 for GraphQL requests with invalid session version
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- `GET /api/oauth` with valid Google `code` exchanges code for access token
|
||||
- OAuth handler fetches Google user profile using access token
|
||||
- `users/find-or-insert!` creates new user if not exists (Google provider, id, email, name, picture)
|
||||
- `users/find-or-insert!` returns existing user if already present
|
||||
- Successful OAuth redirects to `state` param (or `/`) with JWT in query string
|
||||
- Successful OAuth establishes server session with `:identity` and `:version`
|
||||
- Failed OAuth (invalid code, network error) returns 401 with error message
|
||||
- Failed OAuth logs warning via mulog
|
||||
- `GET /logout` clears session and redirects to `/login`
|
||||
- `GET /impersonate?jwt=<token>` with admin session unsigns JWT and sets `:identity` in session
|
||||
- Impersonation JWT must be signed with `:jwt-secret` and `:hs512` algorithm
|
||||
- Non-admin requests to `/impersonate` receive 302 redirect to `/login`
|
||||
- Unauthenticated requests to `/impersonate` receive redirect to `/login`
|
||||
- Session includes `:version` matching `session-version/current-session-version`
|
||||
- Outdated session version triggers redirect to `/login` on normal routes
|
||||
- Outdated session version triggers `hx-redirect` to `/login` on HTMX routes
|
||||
- Outdated session version returns 401 for GraphQL routes
|
||||
|
||||
### UI Test Behaviors (SSR)
|
||||
|
||||
#### Happy Path: OAuth Login
|
||||
1. Unauthenticated user navigates to `/login`
|
||||
2. User clicks "Sign in with Google" button
|
||||
3. Browser redirects to Google OAuth consent screen
|
||||
4. User consents and Google redirects to `/api/oauth?code=<code>&state=<state>`
|
||||
5. Server exchanges code for token, fetches profile, finds/creates user
|
||||
6. Server redirects to original page (or `/`) with `?jwt=<token>`
|
||||
7. Client page reads JWT and establishes session
|
||||
8. User is authenticated and sees their clients/data
|
||||
|
||||
#### Logout
|
||||
1. Authenticated user clicks logout link
|
||||
2. Browser navigates to `/logout`
|
||||
3. Session is cleared
|
||||
4. Browser redirects to `/login`
|
||||
5. User is unauthenticated; subsequent requests redirect to login
|
||||
|
||||
#### Impersonation (Admin)
|
||||
1. Admin user navigates to `/impersonate?jwt=<signed-jwt>`
|
||||
2. Server validates JWT signature and extracts identity
|
||||
3. Session is updated to impersonated user's identity
|
||||
4. Admin sees the application as the impersonated user
|
||||
5. Admin's original session is replaced (no stack)
|
||||
|
||||
#### Unauthenticated Access Attempt
|
||||
1. Unauthenticated user navigates to a protected page (e.g., `/`)
|
||||
2. `wrap-secure` detects missing authentication
|
||||
3. Browser redirects to `/login?redirect-to=<original-path>`
|
||||
4. After successful OAuth, user is redirected back to original page
|
||||
|
||||
#### HTMX Unauthenticated Request
|
||||
1. Authenticated user's session expires
|
||||
2. HTMX request fires (e.g., card refresh)
|
||||
3. `wrap-secure` detects missing authentication
|
||||
4. Response includes `hx-redirect: /login`
|
||||
5. HTMX redirects the browser to login page
|
||||
|
||||
#### Session Version Mismatch
|
||||
1. User has old session (version != current)
|
||||
2. User navigates to any SSR route
|
||||
3. `wrap-session-version` detects mismatch
|
||||
4. Session is invalidated
|
||||
5. User is redirected to `/login`
|
||||
|
||||
## Edge Cases
|
||||
|
||||
### OAuth
|
||||
- **Missing code parameter**: Returns 401 "Couldn't authenticate"
|
||||
- **Invalid/expired code**: Google token endpoint returns error; handler catches exception and returns 401
|
||||
- **Network failure to Google**: Exception caught, logged, 401 returned
|
||||
- **Missing state parameter**: Redirects to `/` (fallback)
|
||||
- **User without email**: Google profile may lack email; `find-or-insert!` handles via provider-id
|
||||
- **Large client list for admin**: `:gz-clients` compresses client data to fit in JWT
|
||||
|
||||
### Session & Permissions
|
||||
- **Session without version**: Treated as outdated by `wrap-session-version`
|
||||
- **Session with nil identity**: Treated as unauthenticated by `wrap-secure`
|
||||
- **Admin with no clients**: Still gets `:gz-clients` (empty list compressed)
|
||||
- **User role with no clients**: Gets `:user/clients` as empty vector
|
||||
- **Read-only user**: Gets `:gz-clients` like admin, but role is `"read-only"`
|
||||
- **JWT tampering**: `jwt/unsign` fails with invalid signature; impersonation fails
|
||||
- **Expired impersonation JWT**: `jwt/unsign` rejects expired token
|
||||
|
||||
### Logout
|
||||
- **Logout without active session**: Still clears session and redirects to login
|
||||
- **Double logout**: Idempotent; session remains empty
|
||||
|
||||
## Test Data Requirements
|
||||
|
||||
### Users
|
||||
- User with `:user/role :user.role/admin`, multiple clients
|
||||
- User with `:user/role :user.role/user`, subset of clients
|
||||
- User with `:user/role :user.role/read-only`, multiple clients
|
||||
- New user (not yet in database) with Google provider details
|
||||
- Existing user with Google provider details
|
||||
|
||||
### Clients
|
||||
- Multiple clients with `:client/code`, `:client/name`, `:client/locations`
|
||||
- Client associations on users
|
||||
|
||||
### OAuth Mock
|
||||
- Mock Google token endpoint responses (success and failure)
|
||||
- Mock Google userinfo endpoint responses
|
||||
|
||||
## Dependencies
|
||||
|
||||
### External Services
|
||||
- **Google OAuth 2.0**: Authorization code exchange and userinfo retrieval
|
||||
- **Buddy (JWT)**: Token signing/unsigning with HS512
|
||||
- **Datomic**: User lookup and creation via `users/find-or-insert!`
|
||||
|
||||
### Frontend Libraries
|
||||
- **Legacy SPA**: Login and needs-activation pages are legacy SPA routes (no SSR tests)
|
||||
|
||||
### Middleware Stack
|
||||
- `wrap-secure`: Authentication gate
|
||||
- `wrap-admin`: Admin-only gate
|
||||
- `wrap-client-redirect-unauthenticated`: Converts 401 to redirects
|
||||
- `wrap-session-version`: Invalidates outdated sessions during SSR rollout
|
||||
|
||||
### Related Subsystems
|
||||
- **Users**: `auto-ap.datomic.users` handles user CRUD
|
||||
- **All SSR Pages**: Every protected route depends on auth middleware
|
||||
341
docs/testing/behaviors/company.md
Normal file
341
docs/testing/behaviors/company.md
Normal file
@@ -0,0 +1,341 @@
|
||||
# Company Behaviors
|
||||
|
||||
## Overview
|
||||
|
||||
The Company section provides company-level settings and reporting for Integreat users. It is implemented as SSR pages using HTMX and is accessed via the left navigation sidebar under "My Company". All pages require authentication and enforce client-level access controls. Most pages react to client selection changes by refreshing their content via HTMX.
|
||||
|
||||
## Routes & Pages
|
||||
|
||||
| Route | Path | Description |
|
||||
|-------|------|-------------|
|
||||
| `:company` | `/company` | Company profile, downloads, signature |
|
||||
| `:company-1099` | `/company/1099` | 1099 vendor report grid |
|
||||
| `:company-reports` | `/company/reports` | Generated reports list |
|
||||
| `:company-expense-report` | `/company/reports/expense` | Expense breakdown and vendor totals |
|
||||
| `:company-reconciliation-report` | `/company/reports/reconciliation` | Bank reconciliation report |
|
||||
| `:company-plaid` | `/company/plaid` | Plaid bank account linking |
|
||||
| `:company-yodlee` | `/company/yodlee` | Yodlee bank account linking |
|
||||
|
||||
### Sub-routes
|
||||
|
||||
- `/company/1099/table` - 1099 vendor data grid (HTMX)
|
||||
- `/company/1099/vendor-dialog/:vendor-id` - Edit vendor 1099 info modal
|
||||
- `/company/1099/vendor-dialog/:vendor-id` (POST) - Save vendor 1099 info
|
||||
- `/company/reports/table` - Reports data grid (HTMX)
|
||||
- `/company/reports/expense/card` - Expense breakdown chart (HTMX)
|
||||
- `/company/reports/expense/invoice-total-card` - Invoice totals grid (HTMX)
|
||||
- `/company/reports/reconciliation/card` - Reconciliation data (HTMX)
|
||||
- `/company/plaid/table` - Plaid accounts grid (HTMX)
|
||||
- `/company/plaid/link` (POST) - Link new Plaid account
|
||||
- `/company/plaid/relink` (PUT) - Initiate Plaid relink
|
||||
- `/company/yodlee/table` - Yodlee accounts grid (HTMX)
|
||||
- `/company/yodlee/fastlink` - Yodlee Fastlink dialog
|
||||
- `/company/yodlee/reauthenticate` (PUT) - Reauthenticate Yodlee account
|
||||
- `/company/yodlee/refresh` (PUT) - Refresh Yodlee provider account (admin only)
|
||||
- `/company/signature/put` - Save drawn signature
|
||||
- `/company/signature/post` - Upload signature file
|
||||
|
||||
## Behaviors by Page
|
||||
|
||||
### Company Profile
|
||||
|
||||
**Route:** `/company`
|
||||
|
||||
#### Client Selection State
|
||||
- **If no client is selected:** Displays a "Please select a company" placeholder card instead of profile content.
|
||||
- **If a client is selected:** Displays the company profile with name, address, downloads, and (if permitted) signature section.
|
||||
|
||||
#### Profile Content
|
||||
- **Company Name:** Displayed as a heading. Pulled from `client/name`.
|
||||
- **Address Block:** Displays street1, street2, city, state, and zip if address data exists. Address fields are optional — missing fields are omitted without error.
|
||||
|
||||
#### Downloads
|
||||
- Shows a "Download vendor list" button.
|
||||
- Clicking downloads a CSV/Excel export via `/api/vendors/company/export?client=<client-code>`.
|
||||
- Button is always visible regardless of permissions.
|
||||
|
||||
#### Signature Upload
|
||||
- **Permission-gated:** Only visible if user has `{:subject :signature :activity :edit}`.
|
||||
- **Existing Signature:** If `client/signature-file` exists, displays the saved signature image (696x261px).
|
||||
- **Draw Signature:**
|
||||
- "New signature" button enables drawing mode on a canvas.
|
||||
- "Clear" button clears the canvas while in drawing mode.
|
||||
- "Accept" button converts the canvas to a PNG data URL and submits it via HTMX PUT to `/company/signature/put`.
|
||||
- Invalid image data (not starting with `data:image/png;base64,`) is rejected with a validation error.
|
||||
- **File Upload:**
|
||||
- Drag-and-drop zone for JPEG files (recommended 696x261 pixels).
|
||||
- Hover states change background color from blue to green.
|
||||
- File selected via input or drop triggers form submission via HTMX POST to `/company/signature/post`.
|
||||
- On success, the signature section refreshes with the uploaded image.
|
||||
- **Storage:** Uploaded signatures are stored in S3 bucket `integreat-signature-images` with public-read ACL. The public URL is saved to `client/signature-file`.
|
||||
|
||||
### 1099 Reports
|
||||
|
||||
**Route:** `/company/1099`
|
||||
|
||||
#### Vendor Grid
|
||||
- Displays vendors who received **$600 or more in check payments during calendar year 2025** (payments dated Jan 1, 2025 - Jan 1, 2026).
|
||||
- Grid columns:
|
||||
- **Client:** Client code
|
||||
- **Vendor Name:** Vendor name with legal entity name (or first/middle/last name) as subtitle. Shows a 1099 type pill badge if set.
|
||||
- **TIN:** Tax ID number with TIN type pill (EIN/SSN)
|
||||
- **Expense Account:** Default expense account name
|
||||
- **Address:** Full address or "No address" placeholder
|
||||
- **Paid:** Total amount paid as a pill badge (rounded to nearest dollar)
|
||||
- Row has an edit (pencil) icon button.
|
||||
|
||||
#### Vendor Edit Dialog
|
||||
- Opens in a modal via HTMX GET.
|
||||
- **Address Section:** Street 1, Street 2, City, State, ZIP. ZIP validated as 5 digits or empty.
|
||||
- **Legal Entity Section:**
|
||||
- Legal Entity Name (full width) — OR —
|
||||
- First Name, Middle Name, Last Name (2-col layout each)
|
||||
- **TIN Section:**
|
||||
- TIN input
|
||||
- TIN Type dropdown: EIN / SSN
|
||||
- 1099 Type dropdown: populated from `legal-entity-1099-type` enum
|
||||
- **Save Behavior:**
|
||||
- Validates via Malli schema.
|
||||
- Address is nulled if all address fields are empty and no `db/id` exists.
|
||||
- On success: modal closes, row refreshes with updated data and flash highlight.
|
||||
|
||||
#### Filtering & Sorting
|
||||
- Supports standard grid query params (sort, pagination, search).
|
||||
- Default sort is by client code then amount.
|
||||
|
||||
### Expense Reports
|
||||
|
||||
**Route:** `/company/reports/expense`
|
||||
|
||||
#### Expense Breakdown Chart
|
||||
- Bar chart showing expenses grouped by **top 20 expense accounts** over the **last 8 weeks**.
|
||||
- X-axis: Week ranges (Monday-Sunday) formatted as dates.
|
||||
- Data sourced from non-voided invoices with expense account allocations.
|
||||
- **Filters:**
|
||||
- Vendor typeahead (filters to specific vendor's invoices)
|
||||
- Account typeahead (filters to specific expense account)
|
||||
- Filters trigger HTMX GET to `/company/reports/expense/card` with `change` event.
|
||||
- Chart rendered via Chart.js on canvas element with Alpine.js init.
|
||||
|
||||
#### Invoice Totals by Vendor
|
||||
- Grid showing total invoice amounts per vendor per company.
|
||||
- **Date Range Filters:** Start date and End date inputs.
|
||||
- Default range: last 30 days.
|
||||
- Grid columns: Vendor name (sticky left), then one column per company with total amount or "-" for zero.
|
||||
- Filters trigger HTMX GET to `/company/reports/expense/invoice-total-card`.
|
||||
- URL query params are pushed to browser history on filter change.
|
||||
|
||||
### Reconciliation Reports
|
||||
|
||||
**Route:** `/company/reports/reconciliation`
|
||||
|
||||
#### Access Control
|
||||
- Navigation link only visible if user has `{:subject :reconciliation-report}` permission.
|
||||
|
||||
#### Report Display
|
||||
- Compares external bank transactions (from Intuit/QuickBooks) against Integreat transactions.
|
||||
- **Date Range:** Start and End date inputs. Must be submitted via "Run" button.
|
||||
- **Grid Columns:**
|
||||
- Bank Account name
|
||||
- Source count (external transactions)
|
||||
- Synced count (found in Integreat)
|
||||
- Approved transactions
|
||||
- Unapproved transactions
|
||||
- Requires feedback transactions
|
||||
- Missing transactions
|
||||
- **Row Styling:**
|
||||
- Green background (`bg-primary-200`) if external count equals synced count.
|
||||
- Red background (`bg-red-200`) if counts mismatch.
|
||||
- **Missing Transactions:** Shown as a count with tooltip button. Clicking reveals a popup table of missing transaction dates and amounts.
|
||||
|
||||
#### Data Sources
|
||||
- Fetches Intuit bank accounts and transactions for selected clients.
|
||||
- Matches transactions by `transaction/id` in Datomic.
|
||||
- Categorizes found transactions by approval status.
|
||||
|
||||
### Plaid Bank Linking
|
||||
|
||||
**Route:** `/company/plaid`
|
||||
|
||||
#### Account Grid
|
||||
- Displays all Plaid-linked items for the selected clients.
|
||||
- Columns:
|
||||
- **Plaid Item:** External item ID
|
||||
- **Integreat ↔ Plaid Status:** Shows integration state. If any linked bank account has failed/unauthorized integration status, displays red pill with error message tooltip. Otherwise green "Success".
|
||||
- **Plaid ↔ Bank Status:** Plaid item status (e.g., "SUCCESS") with last updated date.
|
||||
- **Accounts:** List of linked accounts with name, masked number, last synced date, and identicon.
|
||||
- Supports sorting by external ID, Plaid bank status.
|
||||
|
||||
#### Link New Account
|
||||
- "Link [client-code] account" button opens Plaid Link modal.
|
||||
- Button only appears when a client is selected.
|
||||
- On successful Plaid Link, HTMX POSTs public token to `/company/plaid/link`.
|
||||
- **Link Handler:**
|
||||
- Exchanges public token for access token.
|
||||
- Fetches accounts from Plaid.
|
||||
- Creates `plaid-item` entity with client reference, external ID, access token, status, and timestamp.
|
||||
- Creates `plaid-account` entities for each account with external ID, masked number, name, and balance.
|
||||
- Redirects back to `/company/plaid` on success.
|
||||
|
||||
#### Re-authenticate
|
||||
- Each row has a "Reauthenticate" button.
|
||||
- Generates a relink token and opens Plaid Link in update mode for the existing item.
|
||||
|
||||
### Yodlee Bank Linking
|
||||
|
||||
**Route:** `/company/yodlee`
|
||||
|
||||
#### Account Grid
|
||||
- Displays Yodlee provider accounts for selected clients.
|
||||
- Columns:
|
||||
- **Client:** Client code (hidden if user has only one client)
|
||||
- **Provider Account:** Yodlee provider account ID
|
||||
- **Status:** Success (green pill) or other (yellow pill)
|
||||
- **Detailed Status:** Raw status string
|
||||
- **Last Updated:** Formatted date
|
||||
- **Accounts:** List of linked accounts with name and number
|
||||
- Supports sorting by status, client, provider account, last updated.
|
||||
|
||||
#### Link New Account
|
||||
- "Link new account" button opens Yodlee Fastlink modal.
|
||||
- Button is disabled if no client is selected, with a note: "Please select a specific customer to link a new account."
|
||||
- Fastlink opens with config "Aggregation" and access token for the selected client.
|
||||
- Error handling: if Yodlee returns an error, displays a notification and closes modal after 3 seconds.
|
||||
|
||||
#### Re-authenticate
|
||||
- "Reauthenticate" button per row opens Fastlink in edit mode for the specific provider account.
|
||||
|
||||
#### Refresh (Admin Only)
|
||||
- Admin users see a refresh icon button on each row.
|
||||
- Triggers Yodlee account refresh and refreshes the row.
|
||||
|
||||
### Reports List
|
||||
|
||||
**Route:** `/company/reports`
|
||||
|
||||
#### Grid
|
||||
- Lists previously generated reports.
|
||||
- Columns:
|
||||
- **Name:** Report name
|
||||
- **Created by:** Creator name as pill badge
|
||||
- **Created:** Formatted creation date
|
||||
- Row actions:
|
||||
- **Download:** Link to S3-hosted report file
|
||||
- **Delete:** Admin-only trash icon. Deletes S3 object and retracts entity.
|
||||
- Supports filtering by date range and client.
|
||||
- Supports sorting by client, created, creator, name.
|
||||
|
||||
## Cross-Cutting Behaviors
|
||||
|
||||
### Client Switching
|
||||
- All company pages listen for `clientSelected from:body` event.
|
||||
- On client switch, HTMX GET refreshes `#app-contents` with a 300ms swap animation.
|
||||
- Pages that require a specific client (Yodlee link, signature, downloads) show appropriate empty/placeholder states when no client is selected.
|
||||
|
||||
### Bank Account Management
|
||||
- **Bank Account Search:** `/company/:db-id/bank-account/search` provides typeahead data for bank accounts belonging to a specific client.
|
||||
- **Bank Account Typeahead:** Component used across the app (e.g., in transaction forms) that shows a search field or "Please select a client" message.
|
||||
|
||||
### Permissions
|
||||
|
||||
| Feature | Required Permission |
|
||||
|---------|-------------------|
|
||||
| All company pages | Authenticated user with client access |
|
||||
| Signature upload | `{:subject :signature :activity :edit}` |
|
||||
| Bank Sync Report nav | `{:subject :reconciliation-report}` |
|
||||
| Delete reports | Admin role |
|
||||
| Yodlee refresh | Admin role (`is-admin?`) |
|
||||
|
||||
### Role Access Summary
|
||||
- **Admin:** Full access to all features.
|
||||
- **Power-user / Manager / User:** Can access all company pages (subject to client assignment), can edit signatures (user role), can view reports.
|
||||
- **Read-only:** Can view ledger pages only; no access to company settings.
|
||||
|
||||
## Edge Cases
|
||||
|
||||
### No Client Selected
|
||||
- Company profile shows "Please select a company" placeholder.
|
||||
- Yodlee "Link new account" button is disabled with helper text.
|
||||
- Plaid "Link account" button is hidden (only shown when client-code exists).
|
||||
- 1099, reports, and other grids operate on `trimmed-clients` (all visible clients) when no single client is selected.
|
||||
|
||||
### Invalid Signature Data
|
||||
- Drawing signature data that does not start with `data:image/png;base64,` throws validation error.
|
||||
- File upload errors are caught and logged but currently do not display user-facing error messages (commented-out error UI).
|
||||
|
||||
### Date Range Filters
|
||||
- Expense breakdown defaults to last 65 days of data but displays last 8 weeks.
|
||||
- Invoice totals default to last 30 days.
|
||||
- Reconciliation report requires explicit date range — shows "Please choose a time range to run the report" if none selected.
|
||||
|
||||
### Empty States
|
||||
- 1099 grid: Empty if no vendors received $600+ in checks during 2025.
|
||||
- Reports grid: Empty if no reports have been generated.
|
||||
- Plaid/Yodlee grids: Empty if no bank accounts are linked.
|
||||
- Reconciliation: Missing transactions tooltip only appears when count > 0.
|
||||
|
||||
### Concurrent Client Filtering
|
||||
- 1099 report sums payments per vendor per client. Vendors shared across multiple clients appear in each client's context.
|
||||
- Report list filters to ensure report's clients intersect with user's visible clients.
|
||||
|
||||
## Test Data Requirements
|
||||
|
||||
### Clients
|
||||
- At least 2 clients with different codes, names, and addresses.
|
||||
- One client with a complete address (street1, street2, city, state, zip).
|
||||
- One client with no address.
|
||||
- One client with an existing signature file URL.
|
||||
- One client with no signature file.
|
||||
|
||||
### Vendors
|
||||
- Vendors with 1099 data (legal entity name, TIN, TIN type, 1099 type, address).
|
||||
- Vendors with and without addresses.
|
||||
- Vendors who received $600+ in check payments in 2025.
|
||||
- Vendors who received less than $600 (should not appear in 1099 grid).
|
||||
- Vendors shared across multiple clients.
|
||||
|
||||
### Payments
|
||||
- Check payments dated within 2025 (Jan 1 - Dec 31) for 1099 testing.
|
||||
- Payments of different types (check, ACH, etc.) — only checks count toward 1099.
|
||||
- Payments to vendors across multiple clients.
|
||||
|
||||
### Invoices
|
||||
- Non-voided invoices with expense account allocations for expense report testing.
|
||||
- Invoices with different vendors and expense accounts.
|
||||
- Invoices dated across multiple weeks for 8-week breakdown.
|
||||
|
||||
### Bank Accounts
|
||||
- Plaid-linked accounts with various statuses (SUCCESS, error states).
|
||||
- Yodlee-linked accounts with various statuses.
|
||||
- Bank accounts with integration status (failed, unauthorized, success).
|
||||
|
||||
### Reports
|
||||
- At least one generated report with name, creator, created date, and S3 URL.
|
||||
- Reports belonging to different clients.
|
||||
|
||||
### Users
|
||||
- Admin user (full access).
|
||||
- User with `signature :edit` permission.
|
||||
- User with `reconciliation-report` permission.
|
||||
- User with single client access.
|
||||
- User with multiple client access.
|
||||
- Read-only user (should not see company nav).
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Backend
|
||||
- Datomic for all entity storage and queries.
|
||||
- Amazonica S3 for signature image storage and report file storage.
|
||||
- Plaid API for bank account linking (token exchange, account fetch).
|
||||
- Yodlee API for bank account linking (Fastlink, token management).
|
||||
- Intuit/QuickBooks API for reconciliation report data.
|
||||
- Solr for client name search (`company-search` endpoint).
|
||||
|
||||
### Frontend
|
||||
- HTMX for all server-rendered interactions.
|
||||
- Alpine.js for signature canvas state, drag-and-drop file upload, modal state, chart initialization.
|
||||
- Chart.js for expense breakdown bar chart.
|
||||
- SignaturePad library for canvas signature drawing.
|
||||
- Plaid Link SDK for Plaid account linking.
|
||||
- Yodlee Fastlink SDK for Yodlee account linking.
|
||||
- Jdenticon for account identicons in Plaid grid.
|
||||
175
docs/testing/behaviors/dashboard.md
Normal file
175
docs/testing/behaviors/dashboard.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# Dashboard Behaviors
|
||||
|
||||
## Overview
|
||||
|
||||
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
|
||||
|
||||
| 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
|
||||
|
||||
### Unit Test Behaviors
|
||||
|
||||
- `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
|
||||
|
||||
### Integration Test Behaviors
|
||||
|
||||
- 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
|
||||
|
||||
### UI Test Behaviors (SSR)
|
||||
|
||||
#### 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
|
||||
|
||||
#### 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."
|
||||
|
||||
## Edge Cases
|
||||
|
||||
### 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)
|
||||
|
||||
### 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
|
||||
|
||||
### 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
|
||||
|
||||
### 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
|
||||
|
||||
### 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`
|
||||
|
||||
## Test Data Requirements
|
||||
|
||||
### Users
|
||||
- Admin user (`:user/role "admin"`) with access to multiple clients
|
||||
- Non-admin user (`:user/role "user"`) to test permission denial
|
||||
|
||||
### 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)
|
||||
|
||||
### 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
|
||||
|
||||
## 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
|
||||
496
docs/testing/behaviors/invoice.md
Normal file
496
docs/testing/behaviors/invoice.md
Normal file
@@ -0,0 +1,496 @@
|
||||
# Invoice Behaviors
|
||||
|
||||
## Overview
|
||||
|
||||
Invoices are the core entity of the Integreat accounts payable system. They represent bills from vendors that clients must pay. The invoice subsystem supports full lifecycle management from creation/import through payment and voiding, with comprehensive filtering, bulk operations, and OCR-based automated entry via Glimpse.
|
||||
|
||||
**User value:**
|
||||
- Track all vendor bills in one place
|
||||
- Pay invoices individually or in bulk via checks, debits, cash, or credit
|
||||
- Import invoices from CSV/PDF files with automatic data extraction
|
||||
- Spread expenses across multiple locations
|
||||
- Manage invoice status (unpaid, paid, voided, scheduled)
|
||||
|
||||
## Routes & Pages
|
||||
|
||||
| Route | Handler | Purpose |
|
||||
|-------|---------|---------|
|
||||
| `GET /invoices` | `::all-page` | List all invoices |
|
||||
| `GET /invoices/unpaid` | `::unpaid-page` | List unpaid invoices |
|
||||
| `GET /invoices/paid` | `::paid-page` | List paid invoices |
|
||||
| `GET /invoices/voided` | `::voided-page` | List voided invoices |
|
||||
| `GET /invoices/table` | `::table` | HTMX table refresh |
|
||||
| `GET /invoices/new` | `::new-wizard` | Create new invoice wizard |
|
||||
| `POST/PUT /invoices/new` | `::new-invoice-submit` | Submit new/edit invoice |
|
||||
| `PUT /invoices/new/navigate` | `::new-wizard-navigate` | Wizard step navigation |
|
||||
| `PUT /invoices/new/due-date` | `::due-date` | Calculate due date from vendor terms |
|
||||
| `PUT /invoices/new/scheduled-payment-date` | `::scheduled-payment-date` | Calculate scheduled payment from vendor autopay |
|
||||
| `PUT /invoices/new/account/prediction` | `::account-prediction` | Show vendor default account suggestion |
|
||||
| `PUT /invoices/new/total` | `::expense-account-total` | Calculate expense account total |
|
||||
| `PUT /invoices/new/balance` | `::expense-account-balance` | Calculate balance vs invoice total |
|
||||
| `GET /invoices/new/account/new` | `::new-wizard-new-account` | Add new expense account row |
|
||||
| `GET /invoices/new/account/location-select` | `::location-select` | Location dropdown for account |
|
||||
| `GET /invoices/pay-button` | `::pay-button` | Render pay button with selected invoice totals |
|
||||
| `GET /invoices/pay` | `::pay-wizard` | Pay selected invoices wizard |
|
||||
| `POST /invoices/pay` | `::pay-submit` | Submit payment |
|
||||
| `POST /invoices/pay/using-credit` | `::pay-using-credit` | Pay using vendor credit balance |
|
||||
| `PUT /invoices/pay/navigate` | `::pay-wizard-navigate` | Pay wizard step navigation |
|
||||
| `GET /invoices/bulk-delete` | `::bulk-delete` | Bulk void confirmation dialog |
|
||||
| `DELETE /invoices/bulk-delete` | `::bulk-delete-confirm` | Execute bulk void |
|
||||
| `GET /invoices/bulk-edit` | `::bulk-edit` | Bulk edit expense accounts wizard |
|
||||
| `PUT /invoices/bulk-edit` | `::bulk-edit-submit` | Submit bulk edit |
|
||||
| `GET /invoices/bulk-edit/account` | `::bulk-edit-new-account` | Add new account row in bulk edit |
|
||||
| `PUT /invoices/bulk-edit/total` | `::bulk-edit-total` | Calculate percentage total |
|
||||
| `PUT /invoices/bulk-edit/balance` | `::bulk-edit-balance` | Calculate percentage balance |
|
||||
| `DELETE /invoices/:id` | `::delete` | Void single invoice |
|
||||
| `PUT /invoices/:id/undo-autopay` | `::undo-autopay` | Undo scheduled autopay |
|
||||
| `PUT /invoices/:id/unvoid` | `::unvoid` | Restore voided invoice |
|
||||
| `PUT /invoices/:id/edit` | `::edit-wizard` | Edit existing invoice |
|
||||
| `GET /invoices/import` | `::import-page` | Import invoices page |
|
||||
| `POST /invoices/import/upload` | `::import-file` | Upload and parse invoice files |
|
||||
| `GET /invoices/import/table` | `::import-table` | Import table refresh |
|
||||
| `DELETE /invoices/import/:id/disapprove` | `::disapprove` | Remove pending imported invoice |
|
||||
| `PUT /invoices/import/:id/approve` | `::approve` | Approve pending imported invoice |
|
||||
| `DELETE /invoices/import/disapprove` | `::bulk-disapprove` | Bulk remove pending imports |
|
||||
| `PUT /invoices/import/approve` | `::bulk-approve` | Bulk approve pending imports |
|
||||
|
||||
## Behaviors by Page
|
||||
|
||||
### Invoice List Page
|
||||
|
||||
#### Unit Tests
|
||||
- `fetch-ids` generates correct Datomic queries for all filter combinations
|
||||
- `fetch-page` returns `[results count outstanding-sum total-sum]` tuple
|
||||
- `sum-outstanding` and `sum-total-amount` calculate aggregates correctly
|
||||
- `selected->ids` resolves "all selected" vs individual selection
|
||||
- `hydrate-results` groups and orders pulled entities by input IDs
|
||||
- `payable-ids` filters to only unpaid, non-scheduled invoices
|
||||
- `can-undo-autopayment` validates invoice state for undo eligibility
|
||||
|
||||
#### Integration Tests
|
||||
- **Happy path:** Load list page with invoices, verify table renders
|
||||
- **Filter by vendor:** Select vendor, table refreshes with filtered results
|
||||
- **Filter by account:** Select account, table refreshes
|
||||
- **Filter by date range:** Apply date range, results filtered
|
||||
- **Filter by amount range:** Set min/max amounts, results filtered
|
||||
- **Filter by invoice number:** Partial match on invoice number
|
||||
- **Filter by check number:** Search by check number
|
||||
- **Exact match:** Navigate from notification to specific invoice via `exact-match-id`
|
||||
- **Sort by each column:** Click column headers (client, vendor, date, due, invoice-number, total, outstanding-balance, location, description-original)
|
||||
- **Pagination:** Navigate pages, change per-page (default 25)
|
||||
- **Selection:** Check individual rows, select all, select all filtered
|
||||
- **Action buttons appear conditionally:** Based on permissions and selection state
|
||||
- **Legacy redirects:** Old routes redirect to new SSR routes
|
||||
|
||||
#### UI Tests (SSR)
|
||||
- Table renders with correct columns: Client, Vendor, Invoice #, Date, Due, Status, Account, Outstanding, Links
|
||||
- Status pills display correctly: Paid (primary), Voided (red), Scheduled (yellow), Unpaid (secondary)
|
||||
- Due date shows relative time: "today", "in X days", "X days ago" with color coding
|
||||
- Outstanding shows partial payment indicator when balance != total
|
||||
- Links dropdown shows payments, transactions, ledger entries, source files
|
||||
- Client column hidden when single client with single location
|
||||
- Row action buttons: Void (unpaid), Edit (unpaid/paid), Unvoid (voided), Undo autopay (eligible)
|
||||
- Pay button tooltip explains why disabled: no selection, multiple clients, mixed debit/credit
|
||||
- Break table grouping by vendor name when sorting by vendor
|
||||
|
||||
### New Invoice Wizard
|
||||
|
||||
#### Unit Tests
|
||||
- `new-form-schema` validates required fields, money amounts, entity IDs
|
||||
- Vendor must have default expense account (`check-vendor-default-account`)
|
||||
- `clientize-vendor` applies client-specific terms, account overrides, autopay settings
|
||||
- `assert-no-conflicting` prevents duplicate invoice numbers per vendor/client
|
||||
- `assert-invoice-amounts-add-up` validates expense accounts sum to total
|
||||
- `maybe-spread-locations` spreads "Shared" location across all client locations
|
||||
- Existing tests in `new_invoice_wizard_test.clj` cover:
|
||||
- Equal spread across 2 locations
|
||||
- Negative amount spreading
|
||||
- Remainder distribution (33.34, 33.33, 33.33)
|
||||
- Negative remainder distribution
|
||||
- Single cent leftover tolerance (`apply-total-delta-to-account`)
|
||||
- `$->cents` and `cents->$` round correctly
|
||||
- `calculate-spread` distributes base amount + remainder
|
||||
|
||||
#### Integration Tests
|
||||
- **Happy path create:**
|
||||
1. Open wizard, select client, vendor, date, invoice number, total
|
||||
2. Due date auto-calculates from vendor terms when client+date+vendor selected
|
||||
3. Scheduled payment auto-calculates from vendor autopay setting
|
||||
4. Save with default account → invoice created with vendor's default account
|
||||
5. Wizard navigates to "Next Steps" with Pay now / Add another / Close options
|
||||
- **Happy path customize accounts:**
|
||||
1. Select "Customize accounts" on basic details
|
||||
2. Add multiple expense account rows
|
||||
3. Select account, location (auto-populated from account or Shared), amount
|
||||
4. Total and balance update dynamically via HTMX
|
||||
5. Location dropdown updates when account changes
|
||||
6. Save validates totals match
|
||||
- **Edit existing invoice:**
|
||||
1. Open edit wizard for unpaid/paid invoice
|
||||
2. Vendor field disabled (cannot change)
|
||||
3. Modify amounts, add/remove accounts
|
||||
4. Save updates row in place
|
||||
|
||||
#### UI Tests (SSR)
|
||||
- Wizard renders as modal with Basic Details → Expense Accounts (optional) → Next Steps
|
||||
- Client typeahead searches companies
|
||||
- Vendor typeahead searches vendors, disabled when editing
|
||||
- Date inputs use normal date format
|
||||
- "Add due / scheduled payment date" link appears when dates not set
|
||||
- Account prediction radio shows vendor default account name
|
||||
- Expense account grid: Account, Location, Amount columns
|
||||
- New account row button adds empty row
|
||||
- Remove row button with Alpine.js transition
|
||||
- Total/Balance rows update on amount change
|
||||
- Validation errors display per field
|
||||
|
||||
### Pay Wizard
|
||||
|
||||
#### Unit Tests
|
||||
- `payment-form-schema` validates payment structure
|
||||
- `does-amount-exceed-outstanding?` catches over/under payments
|
||||
- `can-handwrite?` validates single vendor and positive balance for handwritten checks
|
||||
- `credit-only?` detects when all vendor totals are negative (credit scenario)
|
||||
- `pay-button*` calculates vendor totals and detects credit payment scenarios
|
||||
|
||||
#### Integration Tests
|
||||
- **Happy path - debit:**
|
||||
1. Select unpaid invoices from same client
|
||||
2. Click Pay → Choose Payment Method modal opens
|
||||
3. Select bank account → click Debit
|
||||
4. Confirm "Pay in full" or switch to "Customize payments"
|
||||
5. Submit → payments created, checks printed if applicable
|
||||
- **Happy path - print check:**
|
||||
1. Select check-type bank account
|
||||
2. Choose "Print check"
|
||||
3. Payment processed, PDF URL returned for printing
|
||||
- **Happy path - cash:**
|
||||
1. Select cash-type bank account
|
||||
2. Payment processed immediately
|
||||
- **Happy path - handwrite check:**
|
||||
1. Single vendor, positive balance required
|
||||
2. Enter check number and date
|
||||
3. Payment created with pending status
|
||||
- **Happy path - credit payment:**
|
||||
1. Select invoices where vendor total is 0 (credit + debit balance)
|
||||
2. Pay button shows "Pay invoices using credit"
|
||||
3. Credit invoices offset payment invoices
|
||||
4. Balance payment created
|
||||
- **Validation:**
|
||||
- Cannot pay invoices from multiple clients
|
||||
- Cannot pay with mixed vendor debit/credit totals
|
||||
- Custom payment amounts cannot exceed outstanding balance
|
||||
- Handwritten check requires check number
|
||||
- Locked client dates blocked
|
||||
|
||||
#### UI Tests (SSR)
|
||||
- Pay button shows count and total amount
|
||||
- Warning pill appears when some selected invoices are locked/scheduled
|
||||
- Bank account cards show type-appropriate icons (cash=green, check=blue, credit=purple)
|
||||
- Tooltip on Pay button explains disabled state
|
||||
- Payment method selection buttons: Print check, With cash, Debit, Handwrite check
|
||||
- Payment details step shows invoice grid with vendor, number, total, pay amount
|
||||
- Mode toggle: "Pay in full" vs "Customize payments"
|
||||
- Success modal shows thumbs up, PDF download link for checks, scaling reminder
|
||||
|
||||
### Bulk Edit
|
||||
|
||||
#### Unit Tests
|
||||
- `assert-percentages-add-up` validates percentages sum to 100%
|
||||
- `maybe-code-accounts` distributes expense accounts by percentage
|
||||
- Shared location spreads across all client locations
|
||||
- Rounds cents correctly with remainder handling
|
||||
- Validates account location matches
|
||||
|
||||
#### Integration Tests
|
||||
- **Happy path:**
|
||||
1. Select invoices via checkboxes
|
||||
2. Click "Bulk Edit" → wizard opens
|
||||
3. Add expense account rows with account, location, percentage
|
||||
4. Submit → all selected invoices coded with new expense accounts
|
||||
- **Locked invoices excluded:** Only invoices with date >= client locked-until are processed
|
||||
- **Validation:**
|
||||
- Percentages must sum to 100%
|
||||
- Account location must match account's configured location
|
||||
|
||||
#### UI Tests (SSR)
|
||||
- Wizard shows count of invoices to be edited
|
||||
- Account typeahead with location dropdown
|
||||
- Percentage input (displayed as whole number, stored as decimal)
|
||||
- Total/Balance percentage display
|
||||
- New account row button
|
||||
|
||||
### Bulk Delete/Void
|
||||
|
||||
#### Unit Tests
|
||||
- `void-invoices-internal`:
|
||||
- Voids cash payments linked to selected invoices
|
||||
- Only voids invoices without linked invoice-payments
|
||||
- Only voids invoices with date >= client locked-until
|
||||
- Sets total, outstanding-balance, and expense account amounts to 0
|
||||
|
||||
#### Integration Tests
|
||||
- **Happy path:**
|
||||
1. Select invoices
|
||||
2. Click "Void selected" → confirmation modal
|
||||
3. Confirm → invoices voided, table refreshes with notification
|
||||
- **Admin only:** Bulk delete requires admin permission
|
||||
- **Validation:**
|
||||
- Paid invoices with linked payments require voiding payments first
|
||||
- Locked dates blocked
|
||||
|
||||
#### UI Tests (SSR)
|
||||
- Red alert icon in confirmation modal
|
||||
- Shows count of invoices to void
|
||||
- Modal closes on success with notification
|
||||
|
||||
### Import Page
|
||||
|
||||
#### Unit Tests
|
||||
- `import->invoice` maps parsed data to invoice entity
|
||||
- `validate-invoice` checks required keys and client access
|
||||
- `match-vendor` finds vendor by code or throws with search hint
|
||||
- `admin-only-if-multiple-clients` flags source-url for admin-only when multiple clients
|
||||
- `upload-schema` validates force-client, force-vendor, force-location, force-chatgpt
|
||||
|
||||
#### Integration Tests
|
||||
- **Happy path CSV/PDF import:**
|
||||
1. Upload file(s) via drag-and-drop or file input
|
||||
2. File parsed (CSV direct, PDF via OCR when allowed)
|
||||
3. Invoices created with pending import status
|
||||
4. Results table shows success/failure per file
|
||||
- **Force overrides:**
|
||||
- Force client overrides parsed client
|
||||
- Force vendor overrides parsed vendor
|
||||
- Force location overrides parsed location
|
||||
- Force ChatGPT restricts to GPT-only parsing
|
||||
- **Approve/disapprove:**
|
||||
- Individual approve → status changes to imported
|
||||
- Individual disapprove → invoice deleted
|
||||
- Bulk approve/disapprove with selection
|
||||
- **Validation:**
|
||||
- Missing client, vendor, date, total → error
|
||||
- No client access → error
|
||||
- Vendor not found → error with search hint
|
||||
|
||||
#### UI Tests (SSR)
|
||||
- Upload zone with drag-and-drop styling (blue idle, green hover)
|
||||
- File list shows selected files
|
||||
- Force client/vendor/location typeaheads
|
||||
- "Only use ChatGPT" checkbox
|
||||
- Results table: File, Result, Template, Sample columns
|
||||
- Green/red styling for success/error
|
||||
- Error list shows first 5 errors
|
||||
- Import table shows pending invoices with approve/disapprove buttons
|
||||
- Filters: vendor, date range, check number, invoice number, amount range
|
||||
|
||||
### Glimpse (OCR Import)
|
||||
|
||||
#### Unit Tests
|
||||
- `textract->textract-invoice` extracts fields from AWS Textract response
|
||||
- `stack-rank` sorts field descriptors by confidence
|
||||
- `clean-customer` normalizes customer names for search
|
||||
- `deduplicate` removes duplicate parsed values
|
||||
- `textract-invoice->invoice` converts to invoice with vendor terms and autopay
|
||||
|
||||
#### Integration Tests
|
||||
- **Happy path:**
|
||||
1. Upload PDF to Glimpse page
|
||||
2. File uploaded to S3, Textract job started
|
||||
3. Poll every 5s while IN_PROGRESS
|
||||
4. On SUCCESS, display extracted fields with alternatives
|
||||
5. User selects client, vendor, date, total, invoice number from alternatives
|
||||
6. Save creates invoice and links to textract job
|
||||
- **Field extraction:**
|
||||
- Total: from AMOUNT_DUE or TOTAL fields
|
||||
- Customer: from CUSTOMER_NUMBER or RECEIVER_NAME (with Solr fallback)
|
||||
- Vendor: from VENDOR_NAME with Solr search
|
||||
- Date: from INVOICE_RECEIPT_DATE, ORDER_DATE, DELIVERY_DATE
|
||||
- Invoice number: from INVOICE_RECEIPT_ID or PO_NUMBER
|
||||
|
||||
#### UI Tests (SSR)
|
||||
- Upload form with Dropzone integration (PDF only)
|
||||
- Progress indicator while analyzing
|
||||
- Side-by-side layout: PDF preview iframe + form
|
||||
- Form fields: Client, Location, Vendor, Date, Total, Invoice Number
|
||||
- Alternatives shown as clickable pills below each field
|
||||
- Disabled fields for client and vendor (must select from alternatives)
|
||||
- Success notification with link to view new invoice
|
||||
|
||||
## Cross-Cutting Behaviors
|
||||
|
||||
### Filtering
|
||||
All list pages support these filters (applied via HTMX with debounce):
|
||||
|
||||
- **Vendor:** Typeahead search, filters to specific vendor
|
||||
- **Account:** Typeahead search, filters invoices with that expense account
|
||||
- **Date range:** Start/end date for invoice date (uses `scan-invoices` for performance)
|
||||
- **Due date range:** Start/end due date
|
||||
- **Amount range:** Min/max total amount (inclusive)
|
||||
- **Invoice number:** Partial string match via `.contains`
|
||||
- **Check number:** Text search
|
||||
- **Status:** Route-param based (all, unpaid, paid, voided) or query-param
|
||||
- **Import status:** Filter by pending/imported
|
||||
- **Exact match:** Navigate to specific invoice by ID (bypasses other filters)
|
||||
- **Scheduled payments:** Filter to invoices with scheduled payment date
|
||||
- **Unresolved:** Invoices missing expense accounts or with unassigned accounts
|
||||
- **Location:** Filter by expense account location
|
||||
|
||||
### Sorting
|
||||
Click column headers to sort. Supported sort keys:
|
||||
- client (by client name)
|
||||
- vendor (by vendor name)
|
||||
- description-original
|
||||
- location (by expense account location)
|
||||
- date
|
||||
- due (nulls last with default 2050-01-01)
|
||||
- invoice-number
|
||||
- total
|
||||
- outstanding-balance
|
||||
|
||||
Default sort: descending by scan-invoices default. Toggle asc/desc.
|
||||
|
||||
### Pagination
|
||||
- Default 25 per page
|
||||
- Configurable per-page
|
||||
- Offset-based pagination
|
||||
- Import table supports up to 250 for "all selected" operations
|
||||
|
||||
### Selection
|
||||
- Individual row checkboxes
|
||||
- "Select all" checkbox selects all visible
|
||||
- "Select all filtered" selects all matching current filters (up to 250)
|
||||
- Selection state maintained via Alpine.js
|
||||
- Pay/Void/Edit/Bulk-Edit buttons use selection
|
||||
|
||||
### Permissions
|
||||
All invoice operations check permissions via `can?`:
|
||||
|
||||
| Activity | Required Permission |
|
||||
|----------|-------------------|
|
||||
| Create invoice | `{:subject :invoice :activity :create}` |
|
||||
| Edit invoice | `{:subject :invoice :activity :edit}` |
|
||||
| Delete/Void invoice | `{:subject :invoice :activity :delete}` |
|
||||
| Pay invoice | `{:subject :invoice :activity :pay}` |
|
||||
| Bulk delete | `{:subject :invoice :activity :bulk-delete}` |
|
||||
| Bulk edit | `{:subject :invoice :activity :bulk-edit}` |
|
||||
| Import | `{:subject :invoice :activity :import}` |
|
||||
|
||||
Additional checks:
|
||||
- `assert-can-see-client` - user must have access to invoice's client
|
||||
- `assert-not-locked` - invoice date must be >= client's locked-until date
|
||||
- Admin-only for bulk delete confirmation
|
||||
|
||||
## Edge Cases
|
||||
|
||||
### Voiding Paid Invoices
|
||||
- Single void: Blocked if invoice has linked non-voided payments. User must void payments first.
|
||||
- Bulk void: Automatically voids linked cash payments before voiding invoices.
|
||||
|
||||
### Unvoiding
|
||||
- Restores invoice to original status, total, outstanding balance, and expense accounts
|
||||
- Uses Datomic history to reconstruct previous state
|
||||
- Requires edit permission and client access
|
||||
|
||||
### Undo Autopay
|
||||
- Only available for paid invoices with scheduled payment and no linked payments
|
||||
- Resets status to unpaid, outstanding to total, clears scheduled payment
|
||||
- Locked dates blocked
|
||||
|
||||
### Paying with Credit
|
||||
- Triggered when selected invoices for single vendor have net 0 balance (credit invoices + payment invoices)
|
||||
- Credit invoices (negative balance) offset payment invoices
|
||||
- Creates balance-credit payment
|
||||
|
||||
### Location Spreading
|
||||
- "Shared" location in expense accounts spreads across all client locations
|
||||
- Base amount = total / location count (integer division in cents)
|
||||
- Remainder distributed 1 cent at a time to first N locations
|
||||
- Total delta applied to first account to handle rounding
|
||||
|
||||
### Negative Amounts
|
||||
- Supported throughout (credit memos, overpayments)
|
||||
- Spread calculations handle negative totals
|
||||
- Pay button validates all-credits-or-debits (can't mix positive and negative vendor totals)
|
||||
|
||||
### Zero Balances
|
||||
- Invoices with 0 outstanding balance show as paid
|
||||
- Zero-balance selections excluded from payment
|
||||
|
||||
### Duplicate Invoice Numbers
|
||||
- `assert-no-conflicting` prevents same invoice number for same vendor/client
|
||||
- Edit wizard preserves existing invoice number unless changed
|
||||
|
||||
### Missing Vendor/Account
|
||||
- New invoice wizard requires vendor with default expense account
|
||||
- Import validation fails if vendor not found
|
||||
- Unresolved filter finds invoices missing expense accounts
|
||||
|
||||
### Locked Clients
|
||||
- Client has `locked-until` date
|
||||
- Invoices with date < locked-until cannot be: edited, paid, voided, imported, approved
|
||||
- Bulk operations filter out locked invoices
|
||||
- Pay wizard shows warning when some selections are locked
|
||||
|
||||
### Large Invoice Counts
|
||||
- `scan-invoices` Datomic query used for efficient date/client filtering
|
||||
- Pagination limits to 25 per page by default
|
||||
- "All selected" capped at 250 for bulk operations
|
||||
- Outstanding/total sums calculated from all-ids, not just page
|
||||
|
||||
## Test Data Requirements
|
||||
|
||||
### Entities Needed
|
||||
- **Clients:** Multiple clients with different locations, some with locked-until dates
|
||||
- **Vendors:** With/without default accounts, terms, autopay settings, account overrides, terms overrides
|
||||
- **Accounts:** Expense accounts with/without invoice allowance, different locations
|
||||
- **Bank Accounts:** Check, cash, credit types, visible/hidden
|
||||
- **Invoices:** Various statuses, dates, amounts, with/without payments, with/without scheduled payments
|
||||
|
||||
### Specific Test Scenarios
|
||||
- Client with multiple locations (for spreading)
|
||||
- Client with single location (client column hidden)
|
||||
- Vendor with terms (auto-due date) and autopay (auto-scheduled payment)
|
||||
- Invoice with partial payment (outstanding < total)
|
||||
- Invoice with linked payment (void blocked)
|
||||
- Voided invoice with history (unvoid test)
|
||||
- Paid invoice with scheduled payment, no linked payment (undo autopay)
|
||||
- Negative balance invoice (credit)
|
||||
- Imported pending invoice (approve/disapprove)
|
||||
- Locked client with pre/post-lock invoices
|
||||
|
||||
### File Upload Test Data
|
||||
- Valid CSV with proper headers
|
||||
- PDF with embedded text
|
||||
- PDF requiring OCR
|
||||
- File with multiple clients (admin-only source-url)
|
||||
- File with missing required fields
|
||||
- File with unmatchable vendor
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **Datomic:** Primary data store, history for unvoid
|
||||
- **AWS S3:** File storage for imports and Glimpse PDFs
|
||||
- **AWS Textract:** OCR for Glimpse PDF processing
|
||||
- **Solr:** Search for vendor/client matching in Glimpse
|
||||
- **HTMX/Alpine.js:** Frontend interactivity
|
||||
- **Bidi:** Route generation
|
||||
- **Malli:** Schema validation for forms and query params
|
||||
- **clj-time:** Date parsing and manipulation
|
||||
|
||||
## Existing Tests to Preserve
|
||||
|
||||
### Unit Tests
|
||||
- `test/clj/auto_ap/ssr/invoice/new_invoice_wizard_test.clj`
|
||||
- `maybe-spread-locations-test` - 6 test cases for location spreading logic
|
||||
|
||||
### Integration Tests
|
||||
- `test/clj/auto_ap/integration/routes/invoice_test.clj`
|
||||
- `import-uploaded-invoices` - import single, duplicate prevention, location override, coded invoice
|
||||
- `test/clj/auto_ap/integration/graphql/invoices.clj`
|
||||
- `test-add-invoice` - add with valid/invalid accounts, vendor special accounts
|
||||
- `edit-invoice` - edit fields, vendor change blocked, conflicting invoice numbers
|
||||
- `edit-expense-accounts` - modify expense accounts
|
||||
- `bulk-change-invoices` - bulk coding with percentages
|
||||
- `void-invoices` - bulk void and unvoid
|
||||
- `void-invoice` - single void and unvoid
|
||||
367
docs/testing/behaviors/ledger.md
Normal file
367
docs/testing/behaviors/ledger.md
Normal file
@@ -0,0 +1,367 @@
|
||||
# Ledger Behaviors
|
||||
|
||||
## Overview
|
||||
|
||||
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
|
||||
|
||||
| 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
|
||||
|
||||
### Ledger Entries List
|
||||
|
||||
**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)
|
||||
|
||||
**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)
|
||||
|
||||
**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
|
||||
|
||||
**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
|
||||
|
||||
**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)
|
||||
|
||||
**Pagination**
|
||||
- Default 25 entries per page
|
||||
- Configurable per-page
|
||||
- Pagination controls at bottom
|
||||
|
||||
**CSV Export**
|
||||
- Exports all matching entries with line-item-level rows
|
||||
- Columns: ID, Client, Vendor, Source, External ID, Date, Amount, Account, Debit, Credit
|
||||
|
||||
### 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
|
||||
|
||||
**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
|
||||
|
||||
**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
|
||||
|
||||
**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
|
||||
|
||||
### External Import
|
||||
|
||||
**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
|
||||
|
||||
**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
|
||||
|
||||
**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
|
||||
|
||||
**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
|
||||
|
||||
### P&L Report
|
||||
|
||||
**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
|
||||
|
||||
**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
|
||||
|
||||
**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
|
||||
|
||||
**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
|
||||
|
||||
### Balance Sheet
|
||||
|
||||
**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
|
||||
|
||||
**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)
|
||||
|
||||
**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
|
||||
|
||||
**Warnings**
|
||||
- Warns if >20 clients selected
|
||||
- Warns about unresolved ledger entries
|
||||
|
||||
**PDF Export**
|
||||
- Generates PDF, uploads to S3 at `reports/balance-sheet/<uuid>/<name>.pdf`
|
||||
- Persists report record in Datomic
|
||||
|
||||
### Cash Flows
|
||||
|
||||
**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
|
||||
|
||||
**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
|
||||
|
||||
**Warnings**
|
||||
- Warns if >20 clients selected
|
||||
- Warns about unresolved ledger entries
|
||||
|
||||
**PDF Export**
|
||||
- Generates PDF, uploads to S3 at `reports/cash-flows/<uuid>/<name>.pdf`
|
||||
- Persists report record in Datomic
|
||||
|
||||
### Investigation
|
||||
|
||||
**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
|
||||
|
||||
**Table behavior**
|
||||
- Uses same query schema as main ledger list
|
||||
- Supports sorting and pagination
|
||||
- Does not push URL state
|
||||
|
||||
## 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
|
||||
|
||||
### 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
|
||||
|
||||
### 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
|
||||
|
||||
### 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
|
||||
|
||||
## Edge Cases
|
||||
|
||||
**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
|
||||
|
||||
**Data locking**
|
||||
- Journal entries cannot be created for dates on or before client's `locked-until` date
|
||||
- External import rejects entries for locked dates
|
||||
|
||||
**Unbalanced entries**
|
||||
- "Show unbalanced" filter computes debit/credit sums per entry and filters to mismatches
|
||||
- Unbalanced entries are still displayed in normal view
|
||||
|
||||
**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)
|
||||
|
||||
**Multi-client reports**
|
||||
- Capped at 20 clients for performance
|
||||
- "All" clients defaults to first 5 for reports
|
||||
- Report names include all selected client names
|
||||
|
||||
**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
|
||||
|
||||
## Test Data Requirements
|
||||
|
||||
**Clients**
|
||||
- At least 2 clients with different locations
|
||||
- Client with `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**
|
||||
- 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
|
||||
|
||||
## 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
|
||||
547
docs/testing/behaviors/legacy-spa.md
Normal file
547
docs/testing/behaviors/legacy-spa.md
Normal file
@@ -0,0 +1,547 @@
|
||||
# Legacy SPA Behaviors
|
||||
|
||||
## Overview
|
||||
|
||||
These pages are rendered client-side via Reagent/Re-frame. They use GraphQL for data fetching. They are being migrated to HTMX SSR. Behavior docs exist for reference but **NO UI tests should be written for these pages until migrated**.
|
||||
|
||||
### Architecture
|
||||
- **Routing**: Bidi client-side routes (`src/cljc/auto_ap/client_routes.cljc`)
|
||||
- **State**: Re-frame subscriptions and events
|
||||
- **Data**: GraphQL via custom `graphql` effect (`auto-ap.effects`)
|
||||
- **Permissions**: `auto-ap.permissions/can?` with role-based checks
|
||||
|
||||
### Role Permissions Summary
|
||||
| Role | Transaction Page | Ledger Page | Vendor Create/Edit |
|
||||
|------|-----------------|-------------|-------------------|
|
||||
| admin | full | full | full |
|
||||
| power-user | full | full | full |
|
||||
| manager | full | blocked | full |
|
||||
| user | full | full | full |
|
||||
| read-only | blocked | full | blocked |
|
||||
|
||||
---
|
||||
|
||||
## Pages
|
||||
|
||||
### Home (`/`)
|
||||
|
||||
#### Purpose
|
||||
Dashboard showing financial overview for the selected client: top expense categories (pie chart), upcoming bills (bar chart), and cash flow projection (interactive bar chart with table).
|
||||
|
||||
#### User Flows
|
||||
1. User lands on `/`
|
||||
2. Page loads data for currently selected client
|
||||
3. If user has access to multiple clients, shows note: "these reports are for [client]. Please choose a specific customer for their report."
|
||||
4. User can switch cash flow range: 7/30/60/90/120/150/180 days
|
||||
5. Cash flow bars are clickable and redirect to unpaid invoices for that date
|
||||
|
||||
#### GraphQL Queries Used
|
||||
```graphql
|
||||
query HomeDashboard($clientId: id) {
|
||||
expense_account_stats(client_id: $clientId) {
|
||||
account { id name }
|
||||
total
|
||||
}
|
||||
invoice_stats(client_id: $clientId) {
|
||||
name
|
||||
paid
|
||||
unpaid
|
||||
}
|
||||
cash_flow(client_id: $clientId) {
|
||||
beginning_balance
|
||||
outstanding_payments
|
||||
invoices_due_soon { due outstanding_balance invoice_number vendor { id name } }
|
||||
upcoming_credits { date amount identifier }
|
||||
upcoming_debits { date amount identifier }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Behaviors to Test (when migrated)
|
||||
- **Happy path**: Dashboard loads with all three chart sections
|
||||
- **Multi-client note**: Displays when user hasn't selected a specific client
|
||||
- **Cash flow ranges**: Switching between 7-180 day views updates chart and table
|
||||
- **Cash flow table**: Shows invoices, upcoming debits/credits with days-until
|
||||
- **Edge case**: No data for client shows empty charts gracefully
|
||||
- **Error states**: GraphQL errors show loading state appropriately
|
||||
|
||||
---
|
||||
|
||||
### Login (`/login`)
|
||||
|
||||
#### Purpose
|
||||
Authentication page with Google OAuth login.
|
||||
|
||||
#### User Flows
|
||||
1. User visits `/login`
|
||||
2. Shows "Login with Google" button
|
||||
3. Button links to Google OAuth with optional `redirect-to` query param
|
||||
4. After auth failure/logout, may show logout reason notification
|
||||
|
||||
#### GraphQL Queries/Mutations Used
|
||||
None.
|
||||
|
||||
#### Behaviors to Test (when migrated)
|
||||
- **Happy path**: Shows login button linking to Google OAuth
|
||||
- **Redirect**: Preserves `redirect-to` query parameter in OAuth URL
|
||||
- **Logout reason**: Displays warning notification when `logout-reason` is set
|
||||
- **Edge case**: Already authenticated user visiting login page
|
||||
|
||||
---
|
||||
|
||||
### Needs Activation (`/needs-activation`)
|
||||
|
||||
#### Purpose
|
||||
Shown when authenticated user's account is not yet activated.
|
||||
|
||||
#### User Flows
|
||||
1. User logs in but account is inactive
|
||||
2. Shows message: "Sorry, your user is not activated yet. Please have Ben Skinner enable your account."
|
||||
3. "here" link clears user state and redirects to login
|
||||
|
||||
#### GraphQL Queries/Mutations Used
|
||||
None.
|
||||
|
||||
#### Behaviors to Test (when migrated)
|
||||
- **Happy path**: Shows activation required message
|
||||
- **Relogin link**: Clears user state and redirects to login
|
||||
|
||||
---
|
||||
|
||||
### Transactions (`/transactions/`, `/transactions/unapproved`, `/transactions/approved`, `/transactions/requires-feedback`, `/transactions/excluded`)
|
||||
|
||||
#### Purpose
|
||||
Transaction list with filtering, editing, and bulk admin operations. Different routes show different approval statuses.
|
||||
|
||||
#### User Flows
|
||||
1. User navigates to transactions page
|
||||
2. Default date filter: last 1 month
|
||||
3. Side bar allows filtering by: vendor, account, bank account, date range, amount range, location, import batch, description, linked status
|
||||
4. Table shows transactions with checkbox selection (admin only)
|
||||
5. Clicking row opens edit side bar
|
||||
6. Admin can: bulk code, bulk delete, suppress, or manual Yodlee import
|
||||
|
||||
#### GraphQL Queries/Mutations Used
|
||||
```graphql
|
||||
query TransactionPage($filters: transaction_filters) {
|
||||
transaction_page(filters: $filters) {
|
||||
data {
|
||||
id amount memo location approval_status check_number is_locked
|
||||
matched_rule { note id }
|
||||
vendor { name id }
|
||||
accounts { id amount location account { name id location numeric_code } }
|
||||
date yodlee_merchant { name yodlee_id id } plaid_merchant { name id }
|
||||
post_date expected_deposit { id date } forecast_match { id identifier }
|
||||
status description_original
|
||||
payment { check_number s3_url id date }
|
||||
client { name id }
|
||||
bank_account { name yodlee_account_id current_balance }
|
||||
}
|
||||
total start end count
|
||||
}
|
||||
}
|
||||
|
||||
mutation EditTransaction($transaction: edit_transaction) {
|
||||
edit_transaction(transaction: $transaction) { ... }
|
||||
}
|
||||
|
||||
mutation DeleteTransactions($filters: transaction_filters, $ids: [id], $suppress: Boolean) {
|
||||
delete_transactions(filters: $filters, ids: $ids, suppress: $suppress) { message }
|
||||
}
|
||||
|
||||
mutation BulkCodeTransactions($filters: transaction_filters, $ids: [id], $vendor: id, $approval_status: transaction_approval_status, $accounts: [edit_percentage_account]) {
|
||||
bulk_code_transactions(filters: $filters, ids: $ids, vendor: $vendor, approval_status: $approval_status, accounts: $accounts) { message }
|
||||
}
|
||||
```
|
||||
|
||||
#### Behaviors to Test (when migrated)
|
||||
- **Happy path**: Loads transaction list with default 1-month filter
|
||||
- **Route variants**: Each approval-status route applies correct default filter
|
||||
- **Filtering**: Side bar filters trigger debounced data refresh (800ms)
|
||||
- **Pagination**: Start/per-page params work correctly
|
||||
- **Edit transaction**: Update vendor, approval status, memo, expense accounts
|
||||
- **Edit validation**: Account total must equal transaction amount; locations must be valid
|
||||
- **Bulk delete (admin)**: Delete selected transactions or all visible transactions
|
||||
- **Bulk suppress (admin)**: Mark transactions as suppressed instead of deleting
|
||||
- **Bulk code (admin)**: Apply vendor/account rules to multiple transactions
|
||||
- **Manual import (admin)**: Import transactions via manual Yodlee import dialog
|
||||
- **Error states**: Locked transactions cannot be edited/deleted; validation errors display inline
|
||||
- **Permissions**: Non-admin users don't see checkboxes or action buttons
|
||||
|
||||
---
|
||||
|
||||
### Ledger (`/ledger/`)
|
||||
|
||||
#### Purpose
|
||||
General ledger showing journal entries with line items. Supports filtering and CSV export.
|
||||
|
||||
#### User Flows
|
||||
1. User navigates to `/ledger/`
|
||||
2. Default date filter: last 1 month
|
||||
3. Side bar filters: vendor, account, bank account, date range, amount range, location
|
||||
4. Table shows journal entries with expandable line items
|
||||
5. Admin can export to CSV
|
||||
|
||||
#### GraphQL Queries/Mutations Used
|
||||
```graphql
|
||||
query LedgerPage($filters: ledger_filters) {
|
||||
ledger_page(filters: $filters) {
|
||||
journal_entries {
|
||||
id source original_entity amount note cleared_against alternate_description
|
||||
vendor { name id } client { name id }
|
||||
line_items { id debit credit location running_balance account { id name } }
|
||||
date
|
||||
}
|
||||
total start end
|
||||
}
|
||||
}
|
||||
|
||||
query LedgerCsv($filters: ledger_filters) {
|
||||
ledger_csv(filters: $filters) { csv_content_b64 }
|
||||
}
|
||||
```
|
||||
|
||||
#### Behaviors to Test (when migrated)
|
||||
- **Happy path**: Loads ledger with default 1-month filter
|
||||
- **Filtering**: Date range, vendor, account, location filters work
|
||||
- **CSV export**: Admin can export filtered results as base64 CSV download
|
||||
- **Pagination**: Virtual pagination controls
|
||||
- **Permissions**: Manager role sees "Not authorized"
|
||||
|
||||
---
|
||||
|
||||
### Profit and Loss (`/ledger/profit-and-loss`)
|
||||
|
||||
#### Purpose
|
||||
Financial report showing revenue and expenses over configurable periods. Supports multi-company and PDF export.
|
||||
|
||||
#### User Flows
|
||||
1. User selects companies (multi-select typeahead)
|
||||
2. User selects period preset (13 periods, 12 months, last week, week-to-date, last month, month-to-date, year-to-date, last calendar year, full year)
|
||||
3. Optional: Include deltas, Column per location
|
||||
4. Click "Run" to generate report
|
||||
5. Admin can "Export" to PDF
|
||||
6. Clicking report cells opens ledger detail side bar
|
||||
|
||||
#### GraphQL Queries/Mutations Used
|
||||
```graphql
|
||||
query ProfitAndLoss($clientIds: [id], $periods: [date_range], $includeDeltas: Boolean, $columnPerLocation: Boolean) {
|
||||
profit_and_loss(client_ids: $clientIds, periods: $periods, include_deltas: $includeDeltas, column_per_location: $columnPerLocation) {
|
||||
periods { accounts { name amount client_id account_type id count numeric_code location } }
|
||||
}
|
||||
}
|
||||
|
||||
query ProfitAndLossPdf($clientIds: [id], $periods: [date_range], $includeDeltas: Boolean, $columnPerLocation: Boolean) {
|
||||
profit_and_loss_pdf(...) { url name }
|
||||
}
|
||||
```
|
||||
|
||||
#### Behaviors to Test (when migrated)
|
||||
- **Happy path**: Generate P&L for single client with default period
|
||||
- **Multi-company**: Select multiple clients and generate combined report
|
||||
- **Period presets**: All preset buttons populate correct date ranges
|
||||
- **Advanced mode**: Custom period date pickers
|
||||
- **Include deltas**: Shows period-over-period changes
|
||||
- **Column per location**: Breaks out by location (mutually exclusive with deltas)
|
||||
- **PDF export**: Admin can generate and download PDF; single-client shows email link
|
||||
- **Ledger drill-down**: Clicking cells opens ledger entries side bar for that account/period
|
||||
- **Permissions**: Manager role sees "Not authorized"
|
||||
|
||||
---
|
||||
|
||||
### Cash Flows (`/ledger/cash-flows`)
|
||||
|
||||
#### Purpose
|
||||
Statement of cash flows report. Same control structure as P&L but different report format.
|
||||
|
||||
#### User Flows
|
||||
Same as Profit and Loss but generates cash flow statement.
|
||||
|
||||
#### GraphQL Queries/Mutations Used
|
||||
```graphql
|
||||
query CashFlows($clientIds: [id], $periods: [date_range], $includeDeltas: Boolean, $columnPerLocation: Boolean) {
|
||||
profit_and_loss(client_ids: $clientIds, periods: $periods, include_deltas: $includeDeltas, column_per_location: $columnPerLocation) {
|
||||
periods { accounts { name amount debits credits client_id account_type id count numeric_code location } }
|
||||
}
|
||||
}
|
||||
|
||||
query CashFlowsPdf($clientIds: [id], $periods: [date_range], $includeDeltas: Boolean, $columnPerLocation: Boolean) {
|
||||
cash_flows_pdf(...) { url name }
|
||||
}
|
||||
```
|
||||
|
||||
#### Behaviors to Test (when migrated)
|
||||
- **Happy path**: Generate cash flows report
|
||||
- **Same controls as P&L**: Companies, periods, deltas, location columns
|
||||
- **PDF export**: Admin-only export button
|
||||
- **Ledger drill-down**: Clicking cells opens ledger detail
|
||||
- **Permissions**: Manager role sees "Not authorized"
|
||||
|
||||
---
|
||||
|
||||
### Profit and Loss Detail (`/ledger/profit-and-loss-detail`)
|
||||
|
||||
#### Purpose
|
||||
Detailed journal entry report broken down by category (sales, COGS, payroll, controllable, fixed overhead, ownership controllable).
|
||||
|
||||
#### User Flows
|
||||
1. Select companies
|
||||
2. Select date range (start/end date pickers)
|
||||
3. Click "Run" to generate detail report
|
||||
4. Shows journal entries grouped by category and account with running balances
|
||||
5. Admin can export to PDF
|
||||
|
||||
#### GraphQL Queries/Mutations Used
|
||||
```graphql
|
||||
query JournalDetailReport($clientIds: [id], $dateRange: date_range, $categories: [category]) {
|
||||
journal_detail_report(client_ids: $clientIds, date_range: $dateRange, categories: $categories) {
|
||||
categories {
|
||||
category client_id location
|
||||
account { numeric_code name }
|
||||
journal_entries { description date debit credit location running_balance account { id name } }
|
||||
total
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query JournalDetailReportPdf($clientIds: [id], $dateRange: date_range, $categories: [category]) {
|
||||
journal_detail_report_pdf(...) { url name }
|
||||
}
|
||||
```
|
||||
|
||||
#### Behaviors to Test (when migrated)
|
||||
- **Happy path**: Generate detail report with default 2-week range
|
||||
- **Category breakdown**: Report includes all 6 category sections plus Gross Profit, Overhead, Net Profit summaries
|
||||
- **Date range filtering**: Start/end dates filter journal entries
|
||||
- **PDF export**: Admin can export to PDF with email link for single client
|
||||
- **Permissions**: Manager role sees "Not authorized"
|
||||
|
||||
---
|
||||
|
||||
### Balance Sheet (`/ledger/balance-sheet`)
|
||||
|
||||
#### Purpose
|
||||
Balance sheet report as of a specific date, with optional prior-year comparison.
|
||||
|
||||
#### User Flows
|
||||
1. Select companies
|
||||
2. Select report date
|
||||
3. Optionally enable "Include comparison" and select comparison date
|
||||
4. Click "Run" to generate balance sheet
|
||||
5. Admin can export to PDF
|
||||
6. Clicking cells opens ledger detail side bar
|
||||
|
||||
#### GraphQL Queries/Mutations Used
|
||||
```graphql
|
||||
query BalanceSheet($clientIds: [id], $date: iso_date, $comparisonDate: iso_date, $includeComparison: Boolean) {
|
||||
balance_sheet(client_ids: $clientIds, date: $date, comparison_date: $comparisonDate, include_comparison: $includeComparison) {
|
||||
balance_sheet_accounts { name amount account_type id numeric_code client_id }
|
||||
comparable_balance_sheet_accounts { name amount account_type id client_id numeric_code }
|
||||
}
|
||||
}
|
||||
|
||||
query BalanceSheetPdf($clientIds: [id], $date: iso_date, $comparisonDate: iso_date, $includeComparison: Boolean) {
|
||||
balance_sheet_pdf(...) { url name }
|
||||
}
|
||||
```
|
||||
|
||||
#### Behaviors to Test (when migrated)
|
||||
- **Happy path**: Generate balance sheet for current date
|
||||
- **Comparison mode**: Shows prior period side-by-side
|
||||
- **Multi-company**: Combines multiple clients
|
||||
- **PDF export**: Admin-only with email composition link for single client
|
||||
- **Ledger drill-down**: Clicking cells opens ledger entries filtered by account and date range
|
||||
- **Permissions**: Manager role sees "Not authorized"
|
||||
|
||||
---
|
||||
|
||||
### External Ledger (`/ledger/external`)
|
||||
|
||||
#### Purpose
|
||||
Shows only externally-imported journal entries. Admin can delete entries.
|
||||
|
||||
#### User Flows
|
||||
1. Admin navigates to `/ledger/external`
|
||||
2. Shows external journal entries with external_id
|
||||
3. Can select entries and delete them
|
||||
4. Admin can export to CSV
|
||||
|
||||
#### GraphQL Queries/Mutations Used
|
||||
```graphql
|
||||
query ExternalLedger($filters: ledger_filters) {
|
||||
ledger_page(filters: $filters) {
|
||||
journal_entries { id external_id source ... }
|
||||
total start end
|
||||
}
|
||||
}
|
||||
|
||||
mutation DeleteExternalLedger($filters: ledger_filters, $ids: [id]) {
|
||||
delete_external_ledger(filters: $filters, ids: $ids) { message }
|
||||
}
|
||||
```
|
||||
|
||||
#### Behaviors to Test (when migrated)
|
||||
- **Happy path**: Loads external-only ledger entries
|
||||
- **Delete**: Admin can delete selected entries (max 1000 at once)
|
||||
- **CSV export**: Same as main ledger
|
||||
- **Permissions**: Admin-only; non-admin sees "Not authorized"
|
||||
|
||||
---
|
||||
|
||||
### External Import (`/ledger/external-import`)
|
||||
|
||||
#### Purpose
|
||||
Manual import of ledger entries via tab-separated text paste.
|
||||
|
||||
#### User Flows
|
||||
1. Admin pastes tab-separated data into textarea
|
||||
2. Optional: indicates whether first row is header
|
||||
3. Click "Parse" to convert to table
|
||||
4. Table shows parsed rows: Id, Client, Source, Vendor, Date, Account, Location, Debit, Credit, Note, Cleared against
|
||||
5. Click "Import" to submit
|
||||
6. Results show: success count, ignored count, error count
|
||||
7. Errors show inline with dropdown explanations
|
||||
8. "Only show errors" filter available
|
||||
|
||||
#### GraphQL Queries/Mutations Used
|
||||
```graphql
|
||||
mutation ImportLedger($entries: [ledger_entry_input]) {
|
||||
import_ledger(entries: $entries) {
|
||||
successful { external_id }
|
||||
existing { external_id }
|
||||
ignored { external_id }
|
||||
errors { external_id error }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Behaviors to Test (when migrated)
|
||||
- **Happy path**: Paste TSV data, parse, import successfully
|
||||
- **Header row**: Checkbox to skip first row
|
||||
- **Validation**: Client code must exist, vendor must exist, date must be MM/dd/yyyy, debits=credits, amounts > 0
|
||||
- **Lock check**: Entries must be after client's locked-until date
|
||||
- **Location validation**: Location must belong to client or be "A"
|
||||
- **Account validation**: Account must exist and location must match account's required location
|
||||
- **Error display**: Errors show per-row with dropdown explanation
|
||||
- **Success/ignored/existing**: Status icons show import result per row
|
||||
- **Permissions**: Admin-only
|
||||
|
||||
---
|
||||
|
||||
### Payments (`/payments/`)
|
||||
|
||||
#### Purpose
|
||||
**Already migrated to SSR.** Legacy client route exists for navbar highlighting only. Actual payments page is served by `payment` SSR routes at `/payment/`.
|
||||
|
||||
#### GraphQL Queries/Mutations Used
|
||||
N/A (SSR page)
|
||||
|
||||
#### Behaviors to Test (when migrated)
|
||||
Already migrated. Test via SSR routes.
|
||||
|
||||
---
|
||||
|
||||
### Reports (`/reports/`)
|
||||
|
||||
#### Purpose
|
||||
**Already migrated to SSR.** Legacy client route exists for navbar highlighting only. Actual reports page is served by `company/reports` SSR routes.
|
||||
|
||||
#### GraphQL Queries/Mutations Used
|
||||
N/A (SSR page)
|
||||
|
||||
#### Behaviors to Test (when migrated)
|
||||
Already migrated. Test via SSR routes.
|
||||
|
||||
---
|
||||
|
||||
### Admin Vendors (`/admin/vendors`)
|
||||
|
||||
#### Purpose
|
||||
**Already migrated to SSR.** Legacy client route exists for navbar highlighting only. Actual vendor management is served by `admin/vendor` SSR routes.
|
||||
|
||||
#### GraphQL Queries/Mutations Used
|
||||
N/A (SSR page)
|
||||
|
||||
#### Behaviors to Test (when migrated)
|
||||
Already migrated. Test via SSR routes.
|
||||
|
||||
---
|
||||
|
||||
### New Vendor (`/vendor/new`)
|
||||
|
||||
#### Purpose
|
||||
Opens the home dashboard with the vendor creation dialog pre-opened.
|
||||
|
||||
#### User Flows
|
||||
1. User navigates to `/vendor/new`
|
||||
2. Loads home dashboard
|
||||
3. Opens vendor dialog if user has `:vendor :create` permission
|
||||
4. Dialog allows creating new vendor with name, terms, address, contacts, default account, etc.
|
||||
|
||||
#### GraphQL Queries/Mutations Used
|
||||
```graphql
|
||||
mutation UpsertVendor($vendor: add_vendor) {
|
||||
upsert_vendor(vendor: $vendor) { id name ... }
|
||||
}
|
||||
```
|
||||
|
||||
#### Behaviors to Test (when migrated)
|
||||
- **Happy path**: Home loads with vendor dialog open
|
||||
- **Permission check**: Dialog only opens if user can `:vendor :create`
|
||||
- **Vendor creation**: Form submission creates vendor via GraphQL mutation
|
||||
- **Validation**: Only one terms override, schedule payment DOM override, and account override per client
|
||||
|
||||
---
|
||||
|
||||
## Cross-Cutting Behaviors
|
||||
|
||||
### GraphQL Query Patterns
|
||||
- All data fetching uses re-frame `graphql` effect with JWT token from `:user` in app-db
|
||||
- Queries use `owns-state` to track loading status per page
|
||||
- Results flow through `::data-page/received` event which stores data and syncs URL query params
|
||||
- Filter changes are debounced (800ms) via `dispatch-debounce`
|
||||
|
||||
### Client-Side State Management (Re-frame)
|
||||
- **Data pages**: `data-page` namespace provides reusable pagination/filtering state
|
||||
- `::data-page/params` - merged filters + table params + query params
|
||||
- `::data-page/data` - GraphQL response data
|
||||
- `::data-page/checked` - selected rows for bulk operations
|
||||
- **Forms**: `forms` namespace manages edit dialogs with `start-form`, `change-handler`, `save-succeeded`
|
||||
- **Status**: `status` namespace tracks async operation states (`:loading`, `:complete`, `:error`)
|
||||
|
||||
### Navigation Between Legacy Pages
|
||||
- Navbar uses Bidi `path-for` with `routes/routes` for legacy SPA links
|
||||
- Some navbar items (Payments, POS, Invoices) now link to `ssr-routes/only-routes` instead
|
||||
- Page components dispatch `::mounted` on mount and `::unmounted` on unmount to set up/tear down:
|
||||
- Data subscriptions
|
||||
- Forward event listeners
|
||||
- Track subscriptions (parameter change watchers)
|
||||
|
||||
---
|
||||
|
||||
## Migration Notes
|
||||
|
||||
### Actively Migrated / Already SSR
|
||||
- **Payments** (`/payments/`) - Fully migrated to `/payment/` SSR routes
|
||||
- **Reports** (`/reports/`) - Fully migrated to `/company/reports` SSR routes
|
||||
- **Admin Vendors** (`/admin/vendors`) - Fully migrated to `/admin/vendor` SSR routes
|
||||
|
||||
### Still Legacy SPA (prioritized by complexity)
|
||||
1. **Transactions** - High complexity (filters, edit form, bulk operations, manual import)
|
||||
2. **Home/Dashboard** - Medium complexity (charts, cash flow calculations)
|
||||
3. **Ledger** - Medium complexity (filters, CSV export)
|
||||
4. **External Ledger** - Low-medium complexity (subset of ledger + delete)
|
||||
5. **External Import** - Medium complexity (TSV parsing, validation, batch import)
|
||||
6. **Profit and Loss** - High complexity (multi-company, periods, PDF export)
|
||||
7. **Cash Flows** - High complexity (shares P&L infrastructure)
|
||||
8. **Profit and Loss Detail** - Medium complexity (category filtering, running balances)
|
||||
9. **Balance Sheet** - Medium complexity (comparison mode, drill-down)
|
||||
10. **Login** - Low complexity (static page)
|
||||
11. **Needs Activation** - Low complexity (static page)
|
||||
12. **New Vendor** - Low complexity (dialog on home page)
|
||||
|
||||
### Migration Risks
|
||||
- **Chart libraries**: Home page uses Recharts (React). Replacement needed for SSR.
|
||||
- **PDF generation**: P&L, Cash Flows, Balance Sheet, P&L Detail all support PDF export via server-side generation.
|
||||
- **Bulk operations**: Transactions page has complex bulk coding/deletion with validation.
|
||||
- **External import**: TSV parsing and validation logic lives entirely in SPA; server-side equivalent needed.
|
||||
179
docs/testing/behaviors/outgoing-invoice.md
Normal file
179
docs/testing/behaviors/outgoing-invoice.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# Outgoing Invoice Behaviors
|
||||
|
||||
## Overview
|
||||
|
||||
The Outgoing Invoice subsystem allows users to create and generate PDF invoices to send to customers. It provides a form-based workflow for specifying the client (sender), recipient details, invoice metadata (date, number, tax rate), and line items. Upon submission, the system calculates subtotals, applies tax, and invokes an AWS Lambda function (`genpdf`) to generate a downloadable PDF.
|
||||
|
||||
## Routes & Pages
|
||||
|
||||
| Route | Method | Handler | Purpose |
|
||||
|-------|--------|---------|---------|
|
||||
| `GET /outgoing-invoice/new` | GET | `::new` | Renders the new outgoing invoice form |
|
||||
| `POST /outgoing-invoice/new` | POST | `::new-submit` | Validates form, generates PDF, shows download modal |
|
||||
| `GET /outgoing-invoice/line-item/new` | GET | `::new-line-item` | Returns HTML for a new empty line item row (HTMX) |
|
||||
|
||||
## Behaviors
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- `form-schema` validates required fields: client, date, to, invoice-number, tax, to-address, line-items
|
||||
- `form-schema` requires line-item description, unit-price, and quantity
|
||||
- `form-schema` makes address street2 optional (nullable string)
|
||||
- `form-schema` coerces line-items vector from form params
|
||||
- `form-schema` applies `strip` decoder to street2 (trims whitespace, treats empty as nil)
|
||||
- `line-item` renders a data-grid row with hidden db/id, description input, quantity money-input, unit-price money-input, and delete button
|
||||
- `line-item` delete button uses Alpine.js to fade out and remove row after 500ms
|
||||
- `submit` filters out line items with empty descriptions
|
||||
- `submit` calculates line-item total as `unit-price * quantity`
|
||||
- `submit` calculates subtotal as sum of all line-item totals
|
||||
- `submit` calculates tax as `subtotal * tax-rate` (tax is a percentage, e.g., 10.0 = 10%)
|
||||
- `submit` calculates total as `subtotal + tax`
|
||||
- `submit` formats monetary values as `$X,XXX.XX` strings before sending to Lambda
|
||||
- `submit` formats invoice date as `normal-date` string before sending to Lambda
|
||||
- `submit` invokes `genpdf` Lambda with JSON payload
|
||||
- `submit` extracts S3 URL from Lambda response and presents download link
|
||||
- `fmt-money` formats nil as `$0.00`
|
||||
- `fmt-money` formats large numbers with comma separators (e.g., `$1,234.56`)
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- `GET /outgoing-invoice/new` returns 200 with full form HTML for authenticated user
|
||||
- Form includes client typeahead, invoice number input, date picker, line items grid, tax input, and recipient address fields
|
||||
- Client typeahead fetches from `:company-search` endpoint
|
||||
- Date input renders with `normal-date` formatted value
|
||||
- Form POSTs to `::new-submit` via HTMX
|
||||
- `POST /outgoing-invoice/new` with valid data returns 200 with modal containing PDF download link
|
||||
- `POST /outgoing-invoice/new` with invalid data returns form with validation errors
|
||||
- Missing required fields (client, date, to, invoice-number) show validation errors
|
||||
- Empty line items (no description) are filtered out before calculation
|
||||
- `GET /outgoing-invoice/line-item/new` returns HTML for a new line item row
|
||||
- New line item row can be appended to the line items grid via HTMX
|
||||
- Multiple line items can be added and each calculates independently
|
||||
- Form is wrapped with `wrap-schema-decode` using `form-schema`
|
||||
- Form is wrapped with `wrap-nested-form-params` to handle nested form parameter keys
|
||||
- Unauthenticated requests redirect to `/login`
|
||||
- Request applies `wrap-trim-client-ids` and `wrap-secure` middleware
|
||||
|
||||
### UI Test Behaviors (SSR)
|
||||
|
||||
#### Happy Path: Create and Generate Invoice
|
||||
1. Authenticated user navigates to outgoing invoice new page
|
||||
2. Page renders with breadcrumbs: Invoices > Outgoing > New
|
||||
3. User selects a client from the typeahead ("From (client)")
|
||||
4. User enters invoice number (e.g., "10000")
|
||||
5. User selects/enters invoice date
|
||||
6. User enters recipient name in "To" field
|
||||
7. User fills in recipient address: street1, city, state, zip (street2 optional)
|
||||
8. User adds line items: clicks "Add line", enters description, quantity, unit price
|
||||
9. User adds multiple line items
|
||||
10. User verifies or adjusts tax percentage (default 10.0)
|
||||
11. User clicks "Generate" button
|
||||
12. Server validates form, calculates totals, invokes PDF Lambda
|
||||
13. Modal appears with message "Download your invoice" and link to S3 URL
|
||||
14. User clicks link to download PDF
|
||||
|
||||
#### Add and Remove Line Items
|
||||
1. User views new invoice form with one default empty line item
|
||||
2. User clicks "Add line" button
|
||||
3. HTMX fetches new line item row and appends it to the grid
|
||||
4. User fills in the new line item
|
||||
5. User clicks delete (X) button on a line item
|
||||
6. Row fades out and removes itself via Alpine.js
|
||||
7. Remaining line items retain their data
|
||||
|
||||
#### Validation Errors
|
||||
1. User clicks "Generate" without filling required fields
|
||||
2. Form POSTs and returns with validation error styling
|
||||
3. Required fields show error indicators
|
||||
4. User fills in missing fields and resubmits
|
||||
5. Invoice generates successfully
|
||||
|
||||
#### Empty Line Items Handling
|
||||
1. User adds multiple line items
|
||||
2. User leaves some line item descriptions blank
|
||||
3. User submits form
|
||||
4. Server filters out empty line items
|
||||
5. PDF generates with only populated line items
|
||||
6. Subtotal and total reflect only populated items
|
||||
|
||||
## Edge Cases
|
||||
|
||||
### Form Validation
|
||||
- **Missing client**: Typeahead shows error state; form cannot submit
|
||||
- **Missing date**: Date input shows error state
|
||||
- **Missing recipient name**: "To" field shows error state
|
||||
- **Missing invoice number**: Invoice # field shows error state
|
||||
- **Invalid date format**: Schema rejects; form redisplays with error
|
||||
- **Negative quantities**: Money input may accept negatives; calculation handles them
|
||||
- **Zero unit price**: Line item total is `$0.00`; included in subtotal
|
||||
- **Zero tax rate**: Tax is `$0.00`; total equals subtotal
|
||||
- **Very large numbers**: Monetary formatting uses commas; Lambda payload handles as string
|
||||
|
||||
### Line Items
|
||||
- **All line items empty**: Subtotal is `0.0`, tax is `0.0`, total is `0.0`
|
||||
- **Single line item**: Calculates correctly
|
||||
- **Many line items (50+)**: Grid scrolls; each item calculates independently
|
||||
- **Delete all line items**: Grid empty; adding new row restores functionality
|
||||
- **Line item with only description**: Filtered out (requires description + values)
|
||||
|
||||
### PDF Generation
|
||||
- **Lambda invocation failure**: Exception propagates; user sees error (no modal)
|
||||
- **Lambda returns invalid JSON**: `json/read-str` may throw; error handling depends on catch logic
|
||||
- **S3 URL inaccessible**: Link is presented but may 403/404 on click
|
||||
- **Very large invoice payload**: Lambda payload size limits may apply
|
||||
|
||||
### Permissions & Auth
|
||||
- **Unauthenticated user**: Redirected to `/login?redirect-to=/outgoing-invoice/new`
|
||||
- **Session expired during form fill**: HTMX POST returns `hx-redirect` to login
|
||||
- **User without client access**: Client typeahead only shows accessible clients
|
||||
|
||||
### Tax Calculation
|
||||
- **Tax as whole number (10)**: Treated as 10% (multiplied by 0.10)
|
||||
- **Tax with decimals (8.25)**: Treated as 8.25%
|
||||
- **Tax over 100%**: Allowed by schema; mathematically valid but business-questionable
|
||||
- **Zero tax**: Total equals subtotal exactly
|
||||
|
||||
## Test Data Requirements
|
||||
|
||||
### Users
|
||||
- Authenticated user with access to at least one client
|
||||
- Client with complete profile including address
|
||||
|
||||
### Clients
|
||||
- Client with `:client/name` and `:client/address` (street, city, state, zip)
|
||||
- Multiple clients to test typeahead selection
|
||||
|
||||
### Form Data
|
||||
- Valid invoice number strings
|
||||
- Valid dates in `normal-date` format
|
||||
- Recipient names and addresses
|
||||
- Line items with descriptions, quantities (numeric), unit prices (monetary)
|
||||
- Tax rates (percentage values, e.g., 10.0 for 10%)
|
||||
|
||||
### AWS Lambda Mock
|
||||
- Mock `genpdf` Lambda invocation returning valid S3 URL
|
||||
- Mock `genpdf` Lambda returning error
|
||||
|
||||
## Dependencies
|
||||
|
||||
### External Services
|
||||
- **AWS Lambda**: `genpdf` function generates PDF from invoice data
|
||||
- **S3**: Generated PDFs stored at `data.prod.app.integreatconsult.com/<path>`
|
||||
- **Datomic**: Client lookup for typeahead, address data
|
||||
|
||||
### Frontend Libraries
|
||||
- **HTMX**: Form submission, line item row fetching
|
||||
- **Alpine.js**: Line item row show/hide and removal animation
|
||||
- **Form cursor (`auto-ap.ssr.form-cursor`)**: Field state management, error binding
|
||||
|
||||
### Middleware Stack
|
||||
- `wrap-secure`: Requires authentication
|
||||
- `wrap-client-redirect-unauthenticated`: Redirects unauthenticated users
|
||||
- `wrap-trim-client-ids`: Trims client IDs from request
|
||||
- `wrap-schema-decode`: Validates and decodes form data against `form-schema`
|
||||
- `wrap-nested-form-params`: Parses nested form parameter structures
|
||||
|
||||
### Related Subsystems
|
||||
- **Invoices**: Outgoing invoices are linked from the main invoices page
|
||||
- **Company/Clients**: Client typeahead depends on company search endpoint
|
||||
- **Company Search**: Typeahead fetches from `:company-search` route
|
||||
236
docs/testing/behaviors/payment.md
Normal file
236
docs/testing/behaviors/payment.md
Normal file
@@ -0,0 +1,236 @@
|
||||
# Payment Behaviors
|
||||
|
||||
## Overview
|
||||
|
||||
Payments represent disbursements to vendors for invoices. The payment system supports multiple payment types (check, cash, debit, credit, balance credit) and tracks payments through statuses (pending, cleared, voided). Payments are created through the invoice payment flow and managed through a searchable grid interface.
|
||||
|
||||
## Routes & Pages
|
||||
|
||||
| Route | Handler | Purpose |
|
||||
|-------|---------|---------|
|
||||
| `GET /payments` | `::all-page` | List all payments |
|
||||
| `GET /payments/pending` | `::pending-page` | List pending payments only |
|
||||
| `GET /payments/cleared` | `::cleared-page` | List cleared payments only |
|
||||
| `GET /payments/voided` | `::voided-page` | List voided payments only |
|
||||
| `GET /payments/table` | `::table` | HTMX table fragment (sort/filter/paginate) |
|
||||
| `DELETE /payments/:id` | `::delete` | Void a single payment |
|
||||
| `GET /payments/bulk-delete` | `::bulk-delete` | Show bulk void confirmation modal |
|
||||
| `DELETE /payments/bulk-delete` | `::bulk-delete-confirm` | Execute bulk void |
|
||||
|
||||
## Behaviors by Page
|
||||
|
||||
### Payment List
|
||||
|
||||
**Grid Display**
|
||||
- Displays payments in a sortable, filterable table with checkboxes for bulk selection
|
||||
- Columns: Client (code), Vendor (name), Bank Account (with icon), Check #, Status, Date, Amount, Links
|
||||
- Client column hidden when viewing a single client's payments
|
||||
- Bank account and date columns hidden on smaller viewports (`show-starting` breakpoints)
|
||||
|
||||
**Status Rendering**
|
||||
- `cleared` — primary-colored pill
|
||||
- `pending` — secondary-colored pill
|
||||
- `voided` — red-colored pill
|
||||
|
||||
**Check Number Links**
|
||||
- When a payment has an `s3-url`, the check number renders as a link opening the PDF in a new tab
|
||||
- Payments without `s3-url` show plain check number text
|
||||
|
||||
**Links Column**
|
||||
- Shows dropdown with links to associated invoices ("Inv. #{number}")
|
||||
- Shows link to associated transaction if one exists ("Transaction")
|
||||
|
||||
**Action Buttons**
|
||||
- "Visible in float" pill — sum of pending payment amounts in current filter view
|
||||
- "Total in float" pill — sum of all pending payments for the selected client(s)
|
||||
- "Void selected" button (red) — only visible to users with `:payment :bulk-delete` permission
|
||||
|
||||
**Row Actions**
|
||||
- Trash icon appears on each row unless payment status is already `voided`
|
||||
- Clicking trash prompts for confirmation ("Are you sure you want to void this payment?")
|
||||
- After void, row is removed from the table with animation
|
||||
|
||||
### Payment Detail
|
||||
|
||||
Individual payments are viewed through the invoice or transaction detail pages. The payment list does not have a dedicated detail page — payments are managed inline from the grid.
|
||||
|
||||
### Bulk Delete (Void)
|
||||
|
||||
**Dialog**
|
||||
- Triggered by clicking "Void selected" with one or more payments checked
|
||||
- Shows warning icon with count of payments to be voided
|
||||
- Supports two selection modes:
|
||||
- **Selected only**: voids only checkboxed payments
|
||||
- **All selected**: voids all payments matching current filters (up to 250)
|
||||
|
||||
**Confirmation**
|
||||
- Clicking "Void payments" in modal executes the bulk void
|
||||
- Modal closes and notification shows: "Successfully voided X of Y payments."
|
||||
|
||||
## Cross-Cutting Behaviors
|
||||
|
||||
### Filtering, Sorting, Pagination
|
||||
|
||||
**Filters (all apply via HTMX with debounced triggers)**
|
||||
- **Vendor**: Typeahead search by vendor name
|
||||
- **Date Range**: Start and end dates
|
||||
- **Check #**: Exact match or partial text search (parsed as Long if possible)
|
||||
- **Invoice #**: Exact match on invoice number
|
||||
- **Amount Range**: Greater-than-or-equal and less-than-or-equal inputs
|
||||
- **Payment Type**: Radio cards — All, Cash, Check, Debit
|
||||
- **Exact Match ID**: Filter to a specific payment by Datomic entity ID
|
||||
|
||||
**Sortable Columns**
|
||||
- Client, Vendor, Bank account, Check number, Date, Amount, Status
|
||||
|
||||
**Pagination**
|
||||
- Default 25 per page
|
||||
- Standard start/per-page query parameters
|
||||
|
||||
**Route-Based Status Filtering**
|
||||
- `/payments/pending` automatically filters to `payment-status/pending`
|
||||
- `/payments/cleared` automatically filters to `payment-status/cleared`
|
||||
- `/payments/voided` automatically filters to `payment-status/voided`
|
||||
- `/payments` shows all statuses
|
||||
|
||||
### Check Printing
|
||||
|
||||
**PDF Generation**
|
||||
- Generates physical check PDFs with MICR encoding at bottom
|
||||
- Includes: payee, amount in numbers and words, date, memo, bank info, client signature image if available
|
||||
- Also generates voucher copies with full invoice details below the check
|
||||
- PDFs stored in S3 under `checks/{uuid}.pdf`
|
||||
|
||||
**Check Numbering**
|
||||
- Check numbers assigned sequentially from `bank-account/check-number`
|
||||
- Bank account's check number incremented by number of vendors paid
|
||||
- Validated: bank account must have a starting check number
|
||||
|
||||
**Batch Processing**
|
||||
- Multiple checks can be merged into single PDF at `merged-checks/{uuid}.pdf`
|
||||
- Invoices grouped by vendor — one check per vendor per batch
|
||||
|
||||
**Validation**
|
||||
- All invoices must belong to same client
|
||||
- Selected bank account must belong to same client
|
||||
- Total amount must be greater than $0.00
|
||||
|
||||
### ACH Payments
|
||||
|
||||
ACH (debit) payments are created with `payment-type/debit`. They create pending payments without generating PDFs or transactions.
|
||||
|
||||
### Balance Credits
|
||||
|
||||
Balance credit payments (`payment-type/balance-credit`) allow paying invoices from existing vendor credit:
|
||||
- Only one vendor at a time
|
||||
- Positive-balance invoices (credits) offset negative-balance invoices (debts)
|
||||
- Creates a single cleared payment for the net amount
|
||||
- Credit invoices are consumed first-in based on outstanding balance
|
||||
|
||||
### Cash Payments
|
||||
|
||||
Cash payments (`payment-type/cash`):
|
||||
- Automatically marked as `cleared`
|
||||
- Creates an associated transaction with `POSTED` status
|
||||
- Transaction uses account with numeric code 21000 (default set)
|
||||
- Payment date set to latest invoice date
|
||||
|
||||
### Voiding Payments
|
||||
|
||||
**Individual Void Rules**
|
||||
- Pending payments can always be voided
|
||||
- Cash, debit, and balance-credit payments can be voided even when cleared
|
||||
- Cleared checks cannot be voided
|
||||
- Cannot void if client is locked (payment date before `client/locked-until`)
|
||||
|
||||
**Void Side Effects**
|
||||
- Payment amount set to `0.0`
|
||||
- Payment status set to `voided`
|
||||
- All `invoice-payment` links removed
|
||||
- Invoice outstanding balances restored
|
||||
- Invoice status reverted to `unpaid` if balance becomes non-zero
|
||||
- Associated transaction unlinked (if present)
|
||||
|
||||
**Bulk Void Rules**
|
||||
- Admin permission required (`:payment :bulk-delete`)
|
||||
- Skips payments that already have transactions (cannot void checks with transactions)
|
||||
- Skips already-voided payments
|
||||
- Respects client lock dates (only voids payments with date >= `locked-until`)
|
||||
- Returns count actually voided vs. requested
|
||||
|
||||
### Permissions
|
||||
|
||||
- **View payments**: User must have visibility to the payment's client
|
||||
- **Void individual payment**: User must be able to see the client
|
||||
- **Bulk void payments**: Admin permission required
|
||||
- **View S3 check PDFs**: Available to all users who can see the payment
|
||||
|
||||
## Edge Cases
|
||||
|
||||
**Zero-Amount Payments**
|
||||
- Check/debit/cash/balance-credit creation rejects if total amount <= $0.00
|
||||
- Void always sets amount to 0.0
|
||||
|
||||
**Locked Clients**
|
||||
- Payments dated before `client/locked-until` cannot be voided
|
||||
- Lock date checked on both individual and bulk void
|
||||
|
||||
**Voided Payments in Float**
|
||||
- Voided payments excluded from float calculations
|
||||
- Only `pending` status payments count toward "Visible in float" and "Total in float"
|
||||
|
||||
**Check Numbers**
|
||||
- Non-check payment types have no check number
|
||||
- Check number search attempts Long parsing; falls back to exact string match
|
||||
|
||||
**Invoice-Payment Restoration**
|
||||
- When voiding, invoice balance is restored by adding back the `invoice-payment/amount`
|
||||
- If restored balance is zero, invoice status remains as-is; otherwise reverts to `unpaid`
|
||||
|
||||
**Filter Combinations**
|
||||
- All filters are AND-combined
|
||||
- Date range uses Datomic scan-payments function for efficient time-bounded queries
|
||||
- Exact-match-id bypasses all other filters
|
||||
|
||||
## Test Data Requirements
|
||||
|
||||
**Basic Payment**
|
||||
```clojure
|
||||
{:db/id "check-id"
|
||||
:payment/check-number 1000
|
||||
:payment/bank-account "bank-id"
|
||||
:payment/client "client-id"
|
||||
:payment/type :payment-type/check
|
||||
:payment/amount 123.50
|
||||
:payment/paid-to "Someone"
|
||||
:payment/status :payment-status/pending
|
||||
:payment/date #inst "2022-01-01"}
|
||||
```
|
||||
|
||||
**Required Entities for Check Printing**
|
||||
- Client with code and at least one bank account
|
||||
- Bank account with `bank-account/check-number` (for checks) or without (for cash/debit)
|
||||
- Vendor with name and default account
|
||||
- Invoice with outstanding balance, vendor, client, and expense accounts
|
||||
- Account with numeric code 21000 (for cash payment transactions)
|
||||
|
||||
**Locked Client Scenario**
|
||||
- Client with `client/locked-until` set to future date
|
||||
- Payment with date before lock date should be unvoidable
|
||||
|
||||
**Multi-Vendor Invoice Set**
|
||||
- Multiple invoices from different vendors
|
||||
- Should group by vendor for check printing
|
||||
- Should reject for balance-credit (single vendor only)
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Datomic for payment/invoice/transaction persistence
|
||||
- S3 for check PDF storage and retrieval
|
||||
- Solr for index updates after payment mutations
|
||||
- HTMX + Alpine.js for interactive grid behavior
|
||||
- `clj-pdf` for check PDF generation
|
||||
- `clj-time` for date parsing and coercion
|
||||
- `auto-ap.datomic/scan-payments` for efficient date-range queries
|
||||
- `auto-ap.permissions/can?` for permission checks
|
||||
- `auto-ap.datomic/audit-transact` for all mutations
|
||||
247
docs/testing/behaviors/pos.md
Normal file
247
docs/testing/behaviors/pos.md
Normal file
@@ -0,0 +1,247 @@
|
||||
# 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.
|
||||
|
||||
## Routes & Pages
|
||||
|
||||
| Page | Route (GET) | Table Route (GET) | Source File |
|
||||
|------|-------------|-------------------|-------------|
|
||||
| Sales Orders | `/pos/sales` | `/pos/sales/table` | `src/clj/auto_ap/ssr/pos/sales_orders.clj` |
|
||||
| Expected Deposits | `/pos/expected-deposit` | `/pos/expected-deposit/table` | `src/clj/auto_ap/ssr/pos/expected_deposits.clj` |
|
||||
| Tenders | `/pos/tenders` | `/pos/tenders/table` | `src/clj/auto_ap/ssr/pos/tenders.clj` |
|
||||
| Refunds | `/pos/refunds` | `/pos/refunds/table` | `src/clj/auto_ap/ssr/pos/refunds.clj` |
|
||||
| Cash Drawer Shifts | `/pos/cash-drawer-shifts` | `/pos/cash-drawer-shifts/table` | `src/clj/auto_ap/ssr/pos/cash_drawer_shifts.clj` |
|
||||
| Sales Summaries (Admin) | `/pos/summaries` | `/pos/summaries/table` | `src/clj/auto_ap/ssr/admin/sales_summaries.clj` |
|
||||
|
||||
All POS pages appear under the **Sales** section in the main sidebar navigation. Sales Summaries uses the admin sidebar and is restricted to admin users.
|
||||
|
||||
## Behaviors by Page
|
||||
|
||||
### Sales Orders
|
||||
|
||||
**Purpose**: Display individual sales orders with line items, charges, and payment breakdowns.
|
||||
|
||||
**Filters**:
|
||||
- Date range (start date / end date)
|
||||
- Total amount range (min / max)
|
||||
- Payment Method radio cards: All, Cash, Card, Gift Card, Other
|
||||
- Processor radio cards: All, Square, Doordash, Uber Eats, Grubhub, Koala, EZCater, No Processor
|
||||
- Category text input (filters by order line item category)
|
||||
|
||||
**Columns**:
|
||||
- Client (hidden when viewing a single client)
|
||||
- Date
|
||||
- Source (rendered as a pill badge)
|
||||
- Total
|
||||
- Tax
|
||||
- Tip
|
||||
- Payment Methods (pills for each unique charge type: cash, card, gift card, other)
|
||||
|
||||
**Sortable by**: client, date, total, tax, tip, source, processor
|
||||
|
||||
**Action Buttons** (above grid): Total $ and Tax $ pills summarizing the currently filtered result set.
|
||||
|
||||
**Row Buttons**: External link icon when `:sales-order/reference-link` exists.
|
||||
|
||||
**Special Behaviors**:
|
||||
- Filter by payment method matches against `charge/type-name` on the order's charges.
|
||||
- Filter by processor matches against `charge/processor`.
|
||||
- Filter by category matches against `order-line-item/category`.
|
||||
- Summation in action buttons uses `auto-ap.datomic.sales-orders/summarize-orders`.
|
||||
|
||||
### Expected Deposits
|
||||
|
||||
**Purpose**: Display expected deposit records from payment processors, with links to transactions.
|
||||
|
||||
**Filters**:
|
||||
- Date range
|
||||
- Exact match ID (shows a removable pill when active)
|
||||
|
||||
**Columns**:
|
||||
- Client (hidden when viewing a single client)
|
||||
- Date
|
||||
- Sales Date
|
||||
- Total
|
||||
- Fee
|
||||
|
||||
**Sortable by**: client, location, date, total, fee
|
||||
|
||||
**Row Buttons**:
|
||||
- External link icon when `:expected-deposit/reference-link` exists
|
||||
- "Transaction" button linking to the associated transaction (if `transaction/_expected-deposit` exists)
|
||||
|
||||
**Special Behaviors**:
|
||||
- Hydration computes a `:totals` breakdown per expected deposit, aggregating charges by sales date with count and amount.
|
||||
|
||||
### Tenders
|
||||
|
||||
**Purpose**: Display individual charge/tender records (payments).
|
||||
|
||||
**Filters**:
|
||||
- Date range
|
||||
- Processor (same radio options as Sales Orders)
|
||||
- Total amount range (min / max)
|
||||
|
||||
**Columns**:
|
||||
- Client (hidden when viewing a single client)
|
||||
- Date
|
||||
- Total
|
||||
- Processor (rendered as a pill badge)
|
||||
- Tip
|
||||
- Links
|
||||
|
||||
**Sortable by**: client, date, total, tip, processor
|
||||
|
||||
**Row Buttons**: External link icon when `:charge/reference-link` exists.
|
||||
|
||||
**Special Behaviors**:
|
||||
- Links column shows an "expected deposit" pill linking to the associated expected deposit if one exists.
|
||||
|
||||
### Refunds
|
||||
|
||||
**Purpose**: Display refund records.
|
||||
|
||||
**Filters**:
|
||||
- Date range
|
||||
- Total amount range (min / max)
|
||||
|
||||
**Columns**:
|
||||
- Client (hidden when viewing a single client)
|
||||
- Date
|
||||
- Total
|
||||
- Type
|
||||
- Fee
|
||||
|
||||
**Sortable by**: client, date, total, fee, type
|
||||
|
||||
**Row Buttons**: None.
|
||||
|
||||
### Cash Drawer Shifts
|
||||
|
||||
**Purpose**: Display cash drawer shift records.
|
||||
|
||||
**Filters**:
|
||||
- Date range
|
||||
- Total amount range (min / max)
|
||||
|
||||
**Columns**:
|
||||
- Client (hidden when viewing a single client)
|
||||
- Date
|
||||
- Paid in
|
||||
- Paid out
|
||||
- Expected cash
|
||||
- Opened cash
|
||||
|
||||
**Sortable by**: client, date, paid-in, paid-out, expected-cash, opened-cash
|
||||
|
||||
**Row Buttons**: None.
|
||||
|
||||
### Sales Summaries
|
||||
|
||||
**Purpose**: Admin-only daily sales summary view with ledger-mapped debit/credit breakdowns. Supports editing account mappings and manual adjustments.
|
||||
|
||||
**Access**: Admin sidebar, wrapped with `wrap-admin` middleware.
|
||||
|
||||
**Filters**:
|
||||
- Date range (currently commented out in UI but schema supports it)
|
||||
|
||||
**Columns**:
|
||||
- Client (hidden when viewing a single client)
|
||||
- Date
|
||||
- Debits (list of debit-line items with category, amount, and "missing account" warning pill if unmapped)
|
||||
- Credits (list of credit-line items with category, amount, and "missing account" warning pill if unmapped)
|
||||
|
||||
**Sortable by**: client, date, debits, credits
|
||||
|
||||
**Row Buttons**: Edit (pencil) icon opens the edit wizard modal.
|
||||
|
||||
**Edit Wizard Behaviors**:
|
||||
- Modal dialog titled "New invoice" (legacy title).
|
||||
- Displays a data grid of summary items with columns: Category, Account, Debits, Credits.
|
||||
- **Auto items** (non-manual): Category and amount are read-only; only Account is editable via typeahead.
|
||||
- **Manual items**: Category (text input), Account (typeahead), Debit amount, Credit amount are all editable.
|
||||
- New manual items can be added via "New Summary Item" row.
|
||||
- Manual items can be removed via X button.
|
||||
- Validation: an item cannot have both credit and debit amounts.
|
||||
- **Total row**: shows running totals for debits and credits.
|
||||
- **Unbalanced row**: shows the difference when debits ≠ credits.
|
||||
- Account typeahead searches accounts scoped to the client with purpose "invoice".
|
||||
- Items missing an account mapping display a red "missing account" pill in the grid view.
|
||||
- Balanced summaries show a green "Total" pill; unbalanced show a red "Total" pill.
|
||||
- Save submits an upsert of `:sales-summary/items`, persisting only `ledger-mapped/account` for auto items and full ledger mapping for manual items.
|
||||
- After save, the row flashes and the modal closes.
|
||||
|
||||
## Cross-Cutting Behaviors
|
||||
|
||||
### Filtering, Sorting, Pagination
|
||||
|
||||
**HTMX Live Filtering**:
|
||||
- All filter forms use `hx-trigger="change delay:500ms, keyup changed from:.hot-filter delay:1000ms"`.
|
||||
- Form changes POST to the table route and target the table container.
|
||||
- The table route swaps the grid contents and updates the browser URL via `hx-push-url`.
|
||||
|
||||
**Date Range**:
|
||||
- All pages support `start-date` and `end-date` query params.
|
||||
- The date range filter is rendered by `auto-ap.ssr.pos.common/date-range-field*`.
|
||||
|
||||
**Total Range**:
|
||||
- Most pages support `total-gte` and `total-lte` (money inputs).
|
||||
|
||||
**Exact Match ID**:
|
||||
- Most pages support `exact-match-id` to jump to a specific record.
|
||||
|
||||
**Sorting**:
|
||||
- Click a sortable column header to toggle ascending/descending.
|
||||
- Multi-sort is supported; active sorts appear as removable pills above the grid.
|
||||
- Remove a sort by clicking the X on its pill.
|
||||
- Default sort is by date descending for most pages.
|
||||
|
||||
**Pagination**:
|
||||
- Default 25 rows per page.
|
||||
- Controls include first/previous/next/last and per-page selector.
|
||||
- Total count is displayed above the grid.
|
||||
|
||||
**Client Scoping**:
|
||||
- All queries are scoped to the user's accessible clients (trimmed to max 20).
|
||||
- Client column is automatically hidden when only one client is in scope.
|
||||
- URL may include `client-id` or `client-code` params.
|
||||
|
||||
### Permissions
|
||||
|
||||
- POS pages require `(can? identity {:subject :sales :activity :read})`.
|
||||
- Sales Summaries requires admin access (`wrap-admin`).
|
||||
- All routes redirect unauthenticated users.
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- **No results**: Grid displays "Total X: 0" with empty rows.
|
||||
- **Single client view**: Client column is hidden; filters and summaries apply to that client's data only.
|
||||
- **Missing reference links**: External link buttons are omitted when no URL exists.
|
||||
- **Missing expected deposit link on Tenders**: Links cell is empty when no associated expected deposit exists.
|
||||
- **Missing transaction on Expected Deposits**: Transaction button is omitted when no linked transaction exists.
|
||||
- **Sales Summaries missing accounts**: Red "missing account" pills appear; totals show red when debits ≠ credits.
|
||||
- **Manual item credit/debit both filled**: Form validation rejects with "Must choose one of credit/debit".
|
||||
- **Date range with no start or end**: Query uses `scan-*` ion functions with nil boundaries.
|
||||
- **Category filter on Sales Orders**: Empty string or nil category param is treated as no filter.
|
||||
|
||||
## Test Data Requirements
|
||||
|
||||
- Clients with `db/id`, `client/name`, `client/code`.
|
||||
- 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`).
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **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.
|
||||
148
docs/testing/behaviors/search-indicators.md
Normal file
148
docs/testing/behaviors/search-indicators.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# Search & Indicators Behaviors
|
||||
|
||||
## Overview
|
||||
|
||||
The Search subsystem provides a global full-text search dialog accessible from the main navigation bar. It queries a Solr index of invoices, payments, transactions, and journal entries, returning results filtered by the user's client permissions. The Indicators subsystem provides small UI utilities like relative date badges (e.g., "5 days ago") used across the application.
|
||||
|
||||
## Routes & Pages
|
||||
|
||||
| Route | Method | Handler | Purpose |
|
||||
|-------|--------|---------|---------|
|
||||
| `GET /search` | GET | `:search` | Opens the search modal dialog |
|
||||
| `POST /search` | POST | `:search` | Executes search query, returns results HTML |
|
||||
| `GET /days-ago` | GET | `::days-ago` | Returns a relative date badge pill (HTMX) |
|
||||
|
||||
## Behaviors
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- `q->solr-q` transforms user query into Solr query string
|
||||
- Bare words become `_text_:"word"` clauses joined with `AND`
|
||||
- Quoted phrases are preserved as single tokens
|
||||
- Keywords `invoice`, `payment`, `transaction`, `journal-entry` map to `type:<value>` filters
|
||||
- Dates in `normal-date` format (e.g., `5/5/2034`) are converted to Solr date format
|
||||
- Dates in `iso-date` format are converted to Solr date format
|
||||
- Unparseable dates pass through unchanged
|
||||
- Decimal numbers (`123.45`) are formatted to 2 decimal places with HALF_UP rounding
|
||||
- Integers pass through unchanged
|
||||
- Multiple tokens are joined with `AND`
|
||||
- `try-cleanse-date` parses `normal-date` and `iso-date` formats, returning Solr-formatted date
|
||||
- `try-cleanse-date` returns original string for unparseable input
|
||||
- `try-parse-number` formats decimal strings to exactly 2 decimal places
|
||||
- `try-parse-number` returns non-decimal strings unchanged
|
||||
- `search-results` filters Solr results by `can-see-client?` against user's allowed clients
|
||||
- `days-ago*` returns "N days ago" pill with color `:primary` for < 30 days
|
||||
- `days-ago*` returns "N days ago" pill with color `:secondary` for 30-59 days
|
||||
- `days-ago*` returns "N days ago" pill with color `:yellow` for 60-89 days
|
||||
- `days-ago*` returns "N days ago" pill with color `:red` for 90+ days
|
||||
- `days-ago*` returns "N days from now" pill with color `:primary` for future dates
|
||||
- `days-ago*` returns empty `[:div]` for nil date
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- `GET /search` with authenticated user returns 200 with search modal HTML
|
||||
- Search modal contains text input with `hx-post="/search"`, `hx-target="#search-results"`, `hx-indicator="#search"`
|
||||
- Search input triggers on `keyup changed delay:300ms` and `search` events
|
||||
- `POST /search` with query parameter `q` returns HTML search results
|
||||
- Search results include card for each Solr document with type icon, client code, amount, vendor name, date, and description
|
||||
- Each result card links to the appropriate detail page (`/invoices`, `/transactions`, `/ledger`, `/payments`) with `exact-match-id` parameter
|
||||
- Results are filtered to only show documents from clients the user can access
|
||||
- `GET /search` without `q` parameter returns modal dialog (not results)
|
||||
- `POST /search` without `q` parameter returns modal dialog (not results)
|
||||
- `GET /days-ago?date=<iso-date>` returns colored pill HTML for past dates
|
||||
- `GET /days-ago?date=<future-date>` returns "days from now" pill HTML
|
||||
- `GET /days-ago` with missing or invalid date returns empty div (schema enforcement)
|
||||
|
||||
### UI Tests (SSR)
|
||||
|
||||
#### Happy Path: Search and View Result
|
||||
1. Authenticated user clicks search icon in navbar
|
||||
2. Search modal opens with autofocused input and placeholder "5/5/2034 Magheritas"
|
||||
3. User types a query (e.g., "invoice 1000")
|
||||
4. After 300ms debounce, HTMX POSTs to `/search`
|
||||
5. Results appear below input as cards
|
||||
6. Each card shows: type icon, type name, client code pill, amount pill, vendor pill (if present), date, and description/number
|
||||
7. User clicks external link icon on a result
|
||||
8. Result opens in new tab on the appropriate detail page with `exact-match-id` set
|
||||
|
||||
#### Search with Type Filter
|
||||
1. User opens search modal
|
||||
2. User types "payment"
|
||||
3. Results are filtered to only show `type:payment` documents
|
||||
4. Each result card shows payment icon and links to `/payments/?exact-match-id=<id>`
|
||||
|
||||
#### Search with Date
|
||||
1. User opens search modal
|
||||
2. User types "5/5/2034"
|
||||
3. Date is parsed and converted to Solr format
|
||||
4. Results matching that date appear
|
||||
|
||||
#### Empty Search Results
|
||||
1. User opens search modal
|
||||
2. User types a query with no matches
|
||||
3. "No results found." message displays
|
||||
|
||||
#### Days-Ago Indicator
|
||||
1. User views a page containing a `days-ago` HTMX element
|
||||
2. Element fetches `/days-ago?date=<date>`
|
||||
3. Colored pill renders showing relative time (e.g., "45 days ago" in secondary color)
|
||||
|
||||
## Edge Cases
|
||||
|
||||
### Search
|
||||
- **Special characters in query**: Solr special characters are not escaped in user query (relies on phrase wrapping)
|
||||
- **Empty query**: Modal renders without results; no Solr query executed
|
||||
- **Very long query**: Passes through to Solr; UI handles long text via flex layout
|
||||
- **No accessible clients**: Returns empty results even if Solr has matching documents
|
||||
- **Solr unavailable**: Behavior depends on `solr/impl` (MockSolrClient returns nil/empty)
|
||||
- **Mixed type keywords and text**: "invoice 1000" produces `type:invoice AND _text_:"1000"`
|
||||
- **Multiple type keywords**: "invoice payment" produces `type:invoice AND type:payment` (likely zero results)
|
||||
- **Numeric tokens with commas/currency**: `$1,000.50` passes through as literal text search
|
||||
- **Future dates**: Date parsing accepts future dates; Solr query includes them
|
||||
|
||||
### Indicators
|
||||
- **Nil date**: Returns empty div, no error
|
||||
- **Invalid date format**: Schema enforcement rejects before handler executes
|
||||
- **Same-day date**: Returns "0 days ago" with primary color
|
||||
- **Very old dates**: Returns "N days ago" with red color (90+ days threshold)
|
||||
|
||||
## Test Data Requirements
|
||||
|
||||
### Solr Index
|
||||
- Indexed documents of all four types: `invoice`, `payment`, `transaction`, `journal-entry`
|
||||
- Documents span multiple clients
|
||||
- Documents with varying dates, amounts, descriptions, numbers, and vendor names
|
||||
- Documents with and without vendor associations
|
||||
|
||||
### Users
|
||||
- Authenticated user with access to subset of clients
|
||||
- Authenticated user with access to all clients
|
||||
- Admin user (for full client visibility)
|
||||
|
||||
### Datomic Entities
|
||||
- Clients with `:client/code` and `:client/name`
|
||||
- Invoices with `:invoice/invoice-number`, `:invoice/total`, `:invoice/date`, `:invoice/client`, `:invoice/vendor`
|
||||
- Payments with `:payment/check-number`, `:payment/amount`, `:payment/date`, `:payment/client`, `:payment/vendor`
|
||||
- Transactions with `:transaction/description-original`, `:transaction/amount`, `:transaction/date`, `:transaction/client`, `:transaction/vendor`
|
||||
- Journal entries with `:journal-entry/amount`, `:journal-entry/date`, `:journal-entry/client`, `:journal-entry/vendor`, `:journal-entry/line-items`
|
||||
|
||||
## Dependencies
|
||||
|
||||
### External Services
|
||||
- **Solr**: Full-text search index (`auto-ap.solr`). Uses `MockSolrClient` in test environments without Solr configured
|
||||
- **Datomic**: Client visibility checks pull user/client associations
|
||||
|
||||
### Frontend Libraries
|
||||
- **HTMX**: Modal loading, search debounce (`keyup changed delay:300ms`), indicator spinner
|
||||
- **Alpine.js**: Modal card structure
|
||||
|
||||
### Middleware Stack
|
||||
- `wrap-secure`: Requires authentication for search and days-ago endpoints
|
||||
- `wrap-client-redirect-unauthenticated`: Redirects unauthenticated to `/login`
|
||||
- `wrap-schema-enforce`: Validates `date` query parameter for `/days-ago`
|
||||
|
||||
### Related Subsystems
|
||||
- **Invoices**: Search results link to invoice detail pages
|
||||
- **Payments**: Search results link to payment detail pages
|
||||
- **Transactions**: Search results link to transaction detail pages
|
||||
- **Ledger**: Search results link to ledger for journal entries
|
||||
301
docs/testing/behaviors/transaction.md
Normal file
301
docs/testing/behaviors/transaction.md
Normal file
@@ -0,0 +1,301 @@
|
||||
# Transaction Behaviors
|
||||
|
||||
## Overview
|
||||
|
||||
Transactions represent bank account activity imported from external sources (Plaid, Yodlee, Intuit). The SSR transaction pages provide an HTMX-based grid view for browsing, filtering, and exporting transactions, plus an admin insights page for AI-assisted vendor/account coding. The legacy client-side application (`/transactions`) remains the primary interface for day-to-day transaction management; the SSR `transaction2` routes are partially implemented.
|
||||
|
||||
## Routes & Pages
|
||||
|
||||
| Route | Handler | Purpose | Status |
|
||||
|-------|---------|---------|--------|
|
||||
| `GET /transaction2` | `::page` | Main transaction grid with filters | Implemented |
|
||||
| `GET /transaction2/table` | `::table` | HTMX-swappable table body | Implemented |
|
||||
| `GET /transaction2/csv` | `::csv` | CSV export of filtered transactions | Implemented |
|
||||
| `GET /transaction2/bank-account-filter` | `::bank-account-filter` | Dynamic bank account radio cards | Implemented |
|
||||
| `GET /transaction2/new` | `::new` | New transaction form | Route defined, handler not wired |
|
||||
| `POST /transaction2/new` | `::new-submit` | Submit new transaction | Route defined, handler not wired |
|
||||
| `GET /transaction2/external-new` | `::external-page` | External transaction entry | Route defined, handler not wired |
|
||||
| `GET /transaction2/external-import-new` | `::external-import-page` | CSV/text import form | Route defined, handler not wired |
|
||||
| `POST /transaction2/external-import-new/parse` | `::external-import-parse` | Parse import data | Route defined, handler not wired |
|
||||
| `POST /transaction2/external-import-new/import` | `::external-import-import` | Execute import | Route defined, handler not wired |
|
||||
| `GET /transaction/insights` | `:transaction-insights` | Admin AI coding insights page | Implemented |
|
||||
| `GET /transaction/insights/table` | `:transaction-insight-table` | Insights data grid | Implemented |
|
||||
| `GET /transaction/insights/rows/:after` | `:transaction-insight-rows` | Infinite scroll rows | Implemented |
|
||||
| `POST /transaction/insights/code/:id` | `:transaction-insight-code` | Approve & code a transaction | Implemented |
|
||||
| `DELETE /transaction/insights/disapprove/:id` | `:transaction-insight-disapprove` | Reject a recommendation | Implemented |
|
||||
| `GET /transaction/insights/explain/:id` | `:transaction-insight-explain` | Similar transactions modal | Implemented |
|
||||
|
||||
## Behaviors by Page
|
||||
|
||||
### Transaction List
|
||||
|
||||
#### Grid Display
|
||||
- Table displays columns: Client, Vendor, Description, Date, Amount, Links
|
||||
- Client column is hidden when only one client with one location is selected
|
||||
- Description column shows `description-original`; when vendor is missing, vendor column falls back to `description-simple` in italics
|
||||
- Date rendered in `normal-date` format (e.g., `MM/DD/YYYY`)
|
||||
- Amount right-aligned and formatted as `$X,XXX.XX`
|
||||
- Links column shows dropdown with links to associated Payment page or Client Overrides
|
||||
- Table supports checkboxes for bulk selection (`:check-boxes? true`)
|
||||
- When sorted by Vendor, table breaks into grouped sections by vendor name (or "No vendor")
|
||||
- Grid title is "Transaction" and entity name is "register"
|
||||
- Breadcrumb shows "Transactions" linking to the list page
|
||||
|
||||
#### Filters
|
||||
- **Vendor**: Typeahead search against vendor names (fetches from `:vendor-search` endpoint)
|
||||
- **Bank Account**: Radio card selector with "All" plus client's bank accounts; dynamically reloads via `clientSelected` event
|
||||
- **Date Range**: Standard date range picker (start/end dates)
|
||||
- **Description**: Free-text input with 1000ms debounced search on `keyup changed`
|
||||
- **Amount Range**: Two money inputs (gte/lte) with "to" label between them
|
||||
- **Exact Match ID**: Hidden filter rendered as a removable pill/tag when present in query params
|
||||
- All filters trigger table reload via HTMX (`change delay:500ms` or `keyup delay:1000ms`)
|
||||
|
||||
#### Sorting
|
||||
- Sortable columns: Client, Vendor, Date, Amount, Description
|
||||
- Default sort is ascending on the implicit `sort-default` field from `scan-transactions`
|
||||
- Sorting is handled server-side with `add-sorter-fields` generating Datomic query clauses
|
||||
- Vendor sort handles missing vendors by grounding empty string
|
||||
|
||||
#### Pagination
|
||||
- Default 25 rows per page
|
||||
- Pagination controls rendered by grid helper
|
||||
- Total matching count and sum of all matching amounts displayed
|
||||
|
||||
#### Actions
|
||||
- "Add Transaction" button (primary color) links to `::route/new` (not yet implemented)
|
||||
- Row action buttons (edit/delete) are commented out in source
|
||||
|
||||
#### CSV Export
|
||||
- CSV route exports all filtered results (not just current page)
|
||||
- CSV headers: Id, Client, Vendor, Description, Date, Amount
|
||||
- CSV uses raw data values (`:db/id`, `:transaction/amount`, etc.) instead of formatted display
|
||||
|
||||
### New Transaction
|
||||
|
||||
> **Note:** Routes are defined in `auto-ap.routes.transactions` but no handlers are wired in `auto-ap.ssr.transaction`. The "Add Transaction" button in the grid links to this route, which would currently 404. Legacy client-side new transaction functionality exists in the SPA.
|
||||
|
||||
Route definitions include:
|
||||
- `GET /transaction2/new` - New transaction form
|
||||
- `POST /transaction2/new` - Form submission
|
||||
- `GET /transaction2/new/location-select` - Location selector sub-component
|
||||
- `GET /transaction2/new/account-typeahead` - Account typeahead sub-component
|
||||
- `GET /transaction2/new/line-item` - Add line item sub-component
|
||||
|
||||
### External Import
|
||||
|
||||
> **Note:** Routes are defined but handlers are not wired in the SSR transaction namespace. The ledger namespace (`auto-ap.ssr.ledger`) has a fully implemented external import flow that these routes may mirror.
|
||||
|
||||
Route definitions include:
|
||||
- `GET /transaction2/external-new` - External transaction entry page
|
||||
- `GET /transaction2/external-import-new` - Import form (CSV/text paste)
|
||||
- `POST /transaction2/external-import-new/parse` - Parse pasted data
|
||||
- `POST /transaction2/external-import-new/import` - Execute import
|
||||
|
||||
The import system (from `auto-ap.import.transactions`) supports:
|
||||
- Deduplication via SHA-256 synthetic keys
|
||||
- Auto-matching to existing pending payments by check number or amount
|
||||
- Auto-matching to expected deposits
|
||||
- Auto-coding via transaction rules
|
||||
- Categorization: `:import`, `:extant`, `:suppressed`, `:error`, `:not-ready`
|
||||
|
||||
### Admin Insights / Coding
|
||||
|
||||
#### Insights Page
|
||||
- Admin-only page at `/transaction/insights`
|
||||
- Title: "Transaction Insights"
|
||||
- Breadcrumbs: Transactions > Insights
|
||||
- Displays data grid card with title "Transaction Insights"
|
||||
- Grid headers: Client, Account, Date, Description, Amount, Actions
|
||||
- No pagination; shows up to 50 recommendations at a time
|
||||
- Infinite scroll via `hx-trigger="intersect once"` on last row
|
||||
- When no more recommendations, shows "That's the last of 'em!"
|
||||
|
||||
#### Recommendation Rows
|
||||
- Shows unapproved transactions from last 300 days that have `outcome-recommendation` data
|
||||
- Each row displays: client code, bank account code, date, description, amount
|
||||
- Amount displayed as rounded dollar tag (green for positive, red for negative)
|
||||
- Up to 3 recommendation buttons per row, sorted by frequency (highest count first)
|
||||
- Each button shows: `Vendor Name | Account Name` with a count badge
|
||||
- Button color is `:primary` if the recommendation was seen by client, `:secondary` otherwise
|
||||
|
||||
#### Coding Actions
|
||||
- **Approve (Code)**: `POST /transaction/insights/code/:id`
|
||||
- Sets approval status to `approved`
|
||||
- Assigns vendor and account
|
||||
- Distributes amount across valid locations using `spread-cents`
|
||||
- Row re-renders with `live-added` class and Alpine.js disappear animation
|
||||
- **Reject (Disapprove)**: `DELETE /transaction/insights/disapprove/:id`
|
||||
- Clears `outcome-recommendation` on transaction
|
||||
- Row re-renders with `live-removed` class and disappear animation
|
||||
- **Explain**: `GET /transaction/insights/explain/:id`
|
||||
- Opens modal showing similar transactions from Pinecone vector search
|
||||
- Displays: Date, Description, Amount, Vendor, Account, Similarity Score
|
||||
- Similarity threshold: score > 0.95
|
||||
- Shows top 10 similar transactions plus the target transaction highlighted
|
||||
|
||||
#### Pinecone Integration
|
||||
- Fetches vector embedding for transaction ID from Pinecone index
|
||||
- Queries for top 100 similar vectors
|
||||
- Filters to scores > 0.95
|
||||
- Enriches matches with vendor name and account numeric code from Datomic
|
||||
|
||||
## Cross-Cutting Behaviors
|
||||
|
||||
### Approval Workflow
|
||||
- Approval statuses: `unapproved`, `approved`, `requires-feedback`, `excluded`, `suppressed`
|
||||
- Transactions start as `unapproved` on import
|
||||
- `suppressed` transactions are excluded from all list queries (including GraphQL)
|
||||
- `requires-feedback` appears in dashboard tasks card
|
||||
- Bulk status changes available via GraphQL mutation `bulk_change_transaction_status` (admin only)
|
||||
- Locked transactions (before `client/locked-until` or `bank-account/start-date`) cannot be modified
|
||||
|
||||
### Coding Transactions to Accounts
|
||||
- Transactions can be coded with one or more expense accounts
|
||||
- Account total must equal 100% of transaction amount (validated server-side)
|
||||
- Location must match account's fixed location if one is set
|
||||
- Location "Shared" distributes amount proportionally across all client locations
|
||||
- Location "A" is reserved for liabilities/equities/assets
|
||||
- Bulk coding available via GraphQL mutation `bulk_code_transactions` (admin only)
|
||||
|
||||
### Bank Account Filtering
|
||||
- Bank account filter only appears when a client is selected
|
||||
- Filter dynamically updates via HTMX when `clientSelected` event fires
|
||||
- Validates that selected bank account belongs to current client (`wrap-ensure-bank-account-belongs`)
|
||||
- Defaults to "All" if selected account doesn't belong to current client
|
||||
|
||||
### CSV Export
|
||||
- CSV route uses same filters as table view
|
||||
- Exports all matching rows (bypasses pagination)
|
||||
- Includes ID column in CSV but not in HTML view
|
||||
|
||||
### Payments & Linking
|
||||
- Transactions can be linked to payments (auto-matched on import by check number or amount)
|
||||
- Linking creates a cleared payment and sets transaction to `approved` with Accounts Payable account
|
||||
- Unlinking a transaction reverts it to `unapproved` and clears payment/accounts
|
||||
- Autopay invoices: transaction can pay multiple invoices, creating a payment that clears them all
|
||||
- Unpaid invoices: similar flow for outstanding balances
|
||||
|
||||
### Permissions
|
||||
- View transactions: `:activity :view :subject :transaction`
|
||||
- Insights page: `:activity :insights :subject :transaction`
|
||||
- Bulk status change: admin only (`assert-admin`)
|
||||
- Bulk coding: admin only
|
||||
- Edit transaction: power user, with client visibility check
|
||||
- Match/unlink transactions: power user
|
||||
- All SSR routes redirect unauthenticated users to `/login`
|
||||
|
||||
### Import Processing
|
||||
- Transactions imported with `transaction->txs` pipeline:
|
||||
1. Assign client and bank account
|
||||
2. Set initial status to `unapproved`
|
||||
3. Extract check number from description if present
|
||||
4. Attempt auto-match to pending payment
|
||||
5. Attempt auto-match to expected deposit
|
||||
6. Apply transaction rules for auto-coding
|
||||
7. Apply default vendor if set
|
||||
- Deduplication via SHA-256 of `date-bank-account-description-amount-index-client`
|
||||
- Suppressed transactions are skipped on re-import
|
||||
|
||||
## Edge Cases
|
||||
|
||||
### Empty Filter Results
|
||||
- Table renders empty state when no transactions match filters
|
||||
- Sum amount shows `$0.00`
|
||||
- Pagination controls still render but show 0 results
|
||||
|
||||
### Invalid Bank Account Selection
|
||||
- If user selects a bank account then switches to a different client that doesn't own that account, filter resets to "All"
|
||||
- `wrap-ensure-bank-account-belongs` middleware enforces this
|
||||
|
||||
### Exact Match ID
|
||||
- When `exact-match-id` is present, all other filters are ignored
|
||||
- Query directly pulls the single transaction by ID (must belong to a visible client)
|
||||
- Renders as a removable pill in the filter area
|
||||
|
||||
### No Client Selected
|
||||
- Bank account filter area renders empty (no radio cards)
|
||||
- All other filters still function
|
||||
- Query uses all visible clients
|
||||
|
||||
### Insights with No Recommendations
|
||||
- Insights page shows empty grid when no unapproved transactions have recommendations
|
||||
- "That's the last of 'em!" message appears at bottom when infinite scroll exhausts results
|
||||
|
||||
### Disappearing Recommendations
|
||||
- After coding or rejecting, the row animates out via Alpine.js (`x-data` with `mount-then-disappear`)
|
||||
- The table does not auto-refresh; user must reload page to see new recommendations
|
||||
|
||||
### Pinecone Unavailable
|
||||
- If Pinecone API fails, the explain modal will show only the target transaction with no similar matches
|
||||
- No explicit error handling for Pinecone HTTP failures
|
||||
|
||||
## Test Data Requirements
|
||||
|
||||
### Users
|
||||
- Admin user with `:user/role "admin"`
|
||||
- Power user with access to specific clients
|
||||
- Regular user with client visibility restrictions
|
||||
- Unauthenticated user (for redirect tests)
|
||||
|
||||
### Clients
|
||||
- Minimum 2 clients with different locations
|
||||
- Client with multiple bank accounts
|
||||
- Client with single location (to test client column hiding)
|
||||
|
||||
### Bank Accounts
|
||||
- Bank account with `bank-account/name` and `bank-account/numeric-code`
|
||||
- Bank account with `bank-account/start-date` (to test locked transactions)
|
||||
- Multiple bank accounts under same client
|
||||
|
||||
### Transactions
|
||||
- Transactions with all approval statuses
|
||||
- Transactions with and without vendors
|
||||
- Transactions with positive and negative amounts
|
||||
- Transactions linked to payments
|
||||
- Transactions with `outcome-recommendation` (for insights)
|
||||
- Transactions with `exact-match-id` parameter
|
||||
- Transactions dated before and after `client/locked-until`
|
||||
|
||||
### Vendors
|
||||
- Vendor with name for typeahead matching
|
||||
- Vendor linked to transactions
|
||||
|
||||
### Accounts
|
||||
- Account with fixed location
|
||||
- Account without fixed location
|
||||
- Account with numeric code (for insights display)
|
||||
|
||||
### Payments
|
||||
- Pending payment with matching check number and amount
|
||||
- Cleared payment linked to transaction
|
||||
|
||||
## Dependencies
|
||||
|
||||
### External Services
|
||||
- **Datomic**: All transaction data stored and queried via Datomic
|
||||
- **Pinecone**: Vector similarity search for transaction insights (explain feature)
|
||||
- **Solr**: Index updated on transaction changes (`solr/touch-with-ledger`)
|
||||
|
||||
### Frontend Libraries
|
||||
- **HTMX**: All SSR interactions (filtering, sorting, pagination, insights coding)
|
||||
- **Alpine.js**: Filter state (exact match pill), row disappear animations
|
||||
- **Chart.js**: Not used on transaction pages
|
||||
|
||||
### Middleware Stack
|
||||
- `wrap-must {:activity :view :subject :transaction}`: Enforces view permission
|
||||
- `wrap-client-redirect-unauthenticated`: Auth redirects
|
||||
- `wrap-schema-enforce`: Validates query params against Malli schema
|
||||
- `wrap-apply-sort`: Applies server-side sorting
|
||||
- `wrap-ensure-bank-account-belongs`: Validates bank account filter
|
||||
- `wrap-copy-qp-pqp`: Copies query params to parsed query params
|
||||
|
||||
### GraphQL Mutations (Legacy/Client-Side)
|
||||
- `bulk_change_transaction_status`: Admin bulk approval status changes
|
||||
- `bulk_code_transactions`: Admin bulk coding with vendor/accounts
|
||||
- `edit_transaction`: Single transaction edit
|
||||
- `match_transaction`: Link to existing payment
|
||||
- `match_transaction_autopay_invoices`: Create payment for autopay invoices
|
||||
- `match_transaction_unpaid_invoices`: Create payment for unpaid invoices
|
||||
- `unlink_transaction`: Remove payment link
|
||||
- `delete_transactions`: Soft delete (suppress) or hard delete
|
||||
- `match_transaction_rules`: Apply transaction rules to matching transactions
|
||||
Reference in New Issue
Block a user