docs: comprehensive test behavior documentation for all pages #6
@@ -4,383 +4,487 @@
|
|||||||
|
|
||||||
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.
|
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
|
**Testing Philosophy**
|
||||||
|
- Prefer unit tests for pure business logic (calculations, validations, transformations)
|
||||||
|
- Use integration tests for database interactions and cross-system flows
|
||||||
|
- Use UI tests only for end-to-end happy paths that touch multiple pages
|
||||||
|
- Every behavior must be user-visible; no tests for implementation details
|
||||||
|
|
||||||
| 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`.
|
## Testing Patterns
|
||||||
|
|
||||||
## Behaviors by Page
|
### Pattern: Grid Page Behaviors
|
||||||
|
Most list pages in Integreat follow the same pattern:
|
||||||
|
1. Fetch IDs via Datomic query with filters
|
||||||
|
2. Hydrate results via `pull-many`
|
||||||
|
3. Render table with sortable columns
|
||||||
|
4. Support selection (individual / all / all-filtered)
|
||||||
|
5. Action buttons appear conditionally based on permissions and selection state
|
||||||
|
|
||||||
### Admin Dashboard
|
**Test implications:** Validate the query generation, not the rendering. UI tests only need to verify one filter and one sort work end-to-end.
|
||||||
|
|
||||||
**Display**
|
### Pattern: Wizard Behaviors
|
||||||
- Shows two Chartist charts:
|
Wizards are multi-step forms with HTMX-driven navigation:
|
||||||
- **Growth in clients**: Bar chart showing client count at 2 years ago, 1 year ago, and today (uses `dc/as-of` against Datomic history).
|
1. Each step is a GET that renders a form fragment
|
||||||
- **Changes by hour**: Line chart showing Datomic transaction counts per hour over the last 24 hours.
|
2. Form submissions are POST/PUT with validation
|
||||||
- Uses the standard admin page layout with `admin-aside-nav`.
|
3. Navigation between steps updates the wizard state
|
||||||
|
4. Final submit creates/updates the entity
|
||||||
|
|
||||||
**Access Control**
|
**Test implications:** Unit test validation logic and state transitions. Integration test the full wizard flow once. UI test only the happy path.
|
||||||
- Only accessible to users with admin role.
|
|
||||||
- Unauthenticated users are redirected to login.
|
|
||||||
|
|
||||||
### Client Management
|
### Pattern: Admin Permission Gates
|
||||||
|
Every admin operation checks:
|
||||||
|
1. `wrap-client-redirect-unauthenticated` — redirects unauthenticated users to login
|
||||||
|
2. `wrap-admin` — blocks non-admin authenticated users
|
||||||
|
3. `assert-can-see-client` — when impersonating, user has access to the client
|
||||||
|
|
||||||
**Grid Display**
|
**Test implications:** Integration test each gate independently. UI tests only verify the happy path with an admin user.
|
||||||
- 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**
|
## Dashboard
|
||||||
- By name, code.
|
|
||||||
- Pagination: 25 per page default.
|
|
||||||
|
|
||||||
**Client Wizard (Create / Edit)**
|
### Display Behaviors
|
||||||
- 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**
|
| # | Behavior | Test Strategy | Status |
|
||||||
- POST for create, PUT for update.
|
|---|----------|---------------|--------|
|
||||||
- Code uniqueness validation on create.
|
| 1.1 | It should display a bar chart showing client count at 2 years ago, 1 year ago, and today | UI | [ ] |
|
||||||
- Groups are upper-cased on save.
|
| 1.2 | It should display a line chart showing Datomic transaction counts per hour over the last 24 hours | UI | [ ] |
|
||||||
- Bank account start dates coerced to dates.
|
| 1.3 | It should render the standard admin page layout with the admin-aside-nav sidebar | UI | [ ] |
|
||||||
- After save: row flashes in grid, modal closes, Solr reindexes client.
|
|
||||||
|
|
||||||
**Biweekly Sales PowerQuery**
|
### Access Control Behaviors
|
||||||
- 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
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 2.1 | It should redirect unauthenticated users to the login page | Integration | [ ] |
|
||||||
|
| 2.2 | It should show an authorization failure for authenticated non-admin users | Integration | [ ] |
|
||||||
|
|
||||||
**Grid Display**
|
---
|
||||||
- Columns: Code (numeric), Name, Type (pill), Location.
|
|
||||||
- Row action: Edit button.
|
|
||||||
- Global action: "New Account" button.
|
|
||||||
|
|
||||||
**Filtering**
|
## Clients
|
||||||
- Name (case-insensitive substring on upper-cased name).
|
|
||||||
- Code (exact numeric match).
|
|
||||||
- Type: All, Dividend, Asset, Equity, Liability, Expense, Revenue, None.
|
|
||||||
|
|
||||||
**Sorting**
|
### Grid Display Behaviors
|
||||||
- By code, name, type, location.
|
|
||||||
- Default sort by upper-cased numeric code.
|
|
||||||
|
|
||||||
**Account Dialog (Create / Edit)**
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Modal card with live-updating header showing code and name.
|
|---|----------|---------------|--------|
|
||||||
- Fields:
|
| 3.1 | It should display a table with columns: Name, Code, Locations, Emails, Status | UI | [ ] |
|
||||||
- Numeric Code (required, unique on create; hidden on edit).
|
| 3.2 | It should show location values as pills in the Locations column | UI | [ ] |
|
||||||
- Name (required).
|
| 3.3 | It should show email values as pills in the Emails column | UI | [ ] |
|
||||||
- Account Type (required, enum: asset, liability, equity, revenue, expense, dividend, none).
|
| 3.4 | It should show lock status as "Locked <date>" with green color when less than 90 days old | UI | [ ] |
|
||||||
- Location (optional string).
|
| 3.5 | It should show lock status with yellow color when between 90 days and 1 year old | UI | [ ] |
|
||||||
- Invoice Allowance (enum: allowed, denied, warn).
|
| 3.6 | It should show lock status with red color when more than 1 year old | UI | [ ] |
|
||||||
- Vendor Allowance (enum: allowed, denied, warn).
|
| 3.7 | It should show "Not locked" in red when no lock date is set | UI | [ ] |
|
||||||
- Applicability (enum: global, customized).
|
| 3.8 | It should show bank account integration status pills in red for failed or unauthorized accounts | UI | [ ] |
|
||||||
- Client Overrides grid: client typeahead + override name. Validates no duplicate clients.
|
| 3.9 | It should show a Biweekly Sales PowerQuery button on each row | UI | [ ] |
|
||||||
|
| 3.10 | It should show an Edit button (pencil icon) on each row | UI | [ ] |
|
||||||
|
| 3.11 | It should show a "New Client" button that opens the client wizard | UI | [ ] |
|
||||||
|
|
||||||
**Save Behavior**
|
### Filtering Behaviors
|
||||||
- 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
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 4.1 | It should filter clients by name using case-insensitive substring match | Integration | [ ] |
|
||||||
|
| 4.2 | It should filter clients by code using exact match on upper-cased code | Integration | [ ] |
|
||||||
|
| 4.3 | It should filter clients by group using exact match on upper-cased group | Integration | [ ] |
|
||||||
|
| 4.4 | It should support an "All" or "Only mine" filter to show only clients assigned to the current user | Integration | [ ] |
|
||||||
|
| 4.5 | It should trigger HTMX requests with 500ms debounce on filter change and 1000ms debounce on keyup | Integration | [ ] |
|
||||||
|
|
||||||
**Grid Display**
|
### Sorting Behaviors
|
||||||
- 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**
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Name (case-insensitive substring on upper-cased name).
|
|---|----------|---------------|--------|
|
||||||
- Type: All, Only hidden, Only global.
|
| 5.1 | It should sort clients by name ascending/descending | Integration | [ ] |
|
||||||
|
| 5.2 | It should sort clients by code ascending/descending | Integration | [ ] |
|
||||||
|
| 5.3 | It should paginate results with 25 clients per page by default | Integration | [ ] |
|
||||||
|
|
||||||
**Vendor Wizard (Create / Edit)**
|
### Client Wizard Behaviors
|
||||||
- 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**
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Modal with Source Vendor (to be deleted) and Target Vendor.
|
|---|----------|---------------|--------|
|
||||||
- Validation: source and target must differ.
|
| 6.1 | It should show a multi-step linear wizard with steps: Info, Matches, Contact, Bank Accounts, Integrations, Cash Flow, Other Settings | UI | [ ] |
|
||||||
- On merge: all references to source vendor are retracted and asserted as target vendor. Source vendor entity is retracted.
|
| 6.2 | It should require Name on the Info step | Integration | [ ] |
|
||||||
- Shows success notification after merge.
|
| 6.3 | It should prevent editing the Code field when editing an existing client | UI | [ ] |
|
||||||
|
| 6.4 | It should allow setting a Locked Until date on the Info step | UI | [ ] |
|
||||||
|
| 6.5 | It should show a dynamic grid for adding and removing locations on the Info step | UI | [ ] |
|
||||||
|
| 6.6 | It should allow configuring string match patterns and location match patterns on the Matches step | UI | [ ] |
|
||||||
|
| 6.7 | It should allow entering address fields and email contacts on the Contact step | UI | [ ] |
|
||||||
|
| 6.8 | It should show a sortable card list of existing bank accounts on the Bank Accounts step | UI | [ ] |
|
||||||
|
| 6.9 | It should allow adding cash accounts with nickname, code, financial code, start date, include-in-reports, and visible-for-payment fields | UI | [ ] |
|
||||||
|
| 6.10 | It should allow adding credit card accounts with bank name, account number, and Plaid/Yodlee/Intuit integration selectors | UI | [ ] |
|
||||||
|
| 6.11 | It should allow adding checking accounts with routing number, bank code, and check number fields | UI | [ ] |
|
||||||
|
| 6.12 | It should require a financial code when "Include in Reports" is enabled for a bank account | Unit + Integration | [ ] |
|
||||||
|
| 6.13 | It should allow entering a Square auth token and mapping Square locations to client locations on the Integrations step | UI | [ ] |
|
||||||
|
| 6.14 | It should show "No locations found" when the Square location refresh times out after 2 seconds | Integration | [ ] |
|
||||||
|
| 6.15 | It should allow entering Week A/B credits and debits on the Cash Flow step | UI | [ ] |
|
||||||
|
| 6.16 | It should allow selecting feature flags and entering groups on the Other Settings step | UI | [ ] |
|
||||||
|
| 6.17 | It should validate that the client code is unique when creating a new client | Unit + Integration | [ ] |
|
||||||
|
| 6.18 | It should upper-case group values on save | Unit | [ ] |
|
||||||
|
| 6.19 | It should flash the updated row in the grid and close the modal after a successful save | UI | [ ] |
|
||||||
|
| 6.20 | It should reindex the client in Solr after a successful save | Integration | [ ] |
|
||||||
|
|
||||||
**Save Behavior**
|
### Biweekly Sales PowerQuery Behaviors
|
||||||
- POST for create, PUT for update.
|
|
||||||
- Solr reindexes vendor name + hidden flag.
|
|
||||||
|
|
||||||
### Transaction Rules
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 7.1 | It should generate 6 saved queries per client (sales summary, category, expected deposits, tenders, refunds, cash drawer shifts) | Integration | [ ] |
|
||||||
|
| 7.2 | It should open a modal with copy-to-clipboard buttons for Excel Power Query M-code | UI | [ ] |
|
||||||
|
|
||||||
**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**
|
## Accounts
|
||||||
- Vendor (entity typeahead).
|
|
||||||
- Note (case-insensitive regex match).
|
|
||||||
- Description (case-insensitive substring).
|
|
||||||
- Client Group (exact upper-cased match).
|
|
||||||
|
|
||||||
**Transaction Rule Wizard**
|
### Grid Display Behaviors
|
||||||
- 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**
|
| # | Behavior | Test Strategy | Status |
|
||||||
- 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.
|
| 8.1 | It should display a table with columns: Code, Name, Type, Location | UI | [ ] |
|
||||||
- Can select "All" or individual transactions.
|
| 8.2 | It should show the account type as a colored pill | UI | [ ] |
|
||||||
- On execution: applies rule coding to each transaction via `upsert-transaction`, touches Solr index.
|
| 8.3 | It should show an Edit button on each row | UI | [ ] |
|
||||||
- Shows notification: "Successfully coded X of Y transactions!"
|
| 8.4 | It should show a "New Account" button | UI | [ ] |
|
||||||
|
|
||||||
**Delete**
|
### Filtering Behaviors
|
||||||
- Confirms before delete.
|
|
||||||
- Retracts entity from Datomic.
|
|
||||||
- Row fades out with "live-removed" class.
|
|
||||||
|
|
||||||
### Background Jobs
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 9.1 | It should filter accounts by name using case-insensitive substring match on upper-cased name | Integration | [ ] |
|
||||||
|
| 9.2 | It should filter accounts by code using exact numeric match | Integration | [ ] |
|
||||||
|
| 9.3 | It should filter accounts by type: All, Dividend, Asset, Equity, Liability, Expense, Revenue, or None | Integration | [ ] |
|
||||||
|
|
||||||
**Grid Display**
|
### Sorting Behaviors
|
||||||
- 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**
|
| # | Behavior | Test Strategy | Status |
|
||||||
- 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:
|
| 10.1 | It should sort accounts by code, name, type, or location ascending/descending | Integration | [ ] |
|
||||||
- Bulk Journal Import: S3 URL path input.
|
| 10.2 | It should default sort by upper-cased numeric code | Integration | [ ] |
|
||||||
- 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
|
### Account Dialog Behaviors
|
||||||
|
|
||||||
**Display**
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Search box for entity ID (numeric Datomic entity ID).
|
|---|----------|---------------|--------|
|
||||||
- History table shows: Date, User, Field, From value, To value.
|
| 11.1 | It should show a modal dialog with a live-updating header displaying the numeric code and name | UI | [ ] |
|
||||||
- Values are formatted: dates → local format, large integers (>1M) → clickable links to that entity's history, nil → "(none)", others → pr-str.
|
| 11.2 | It should require a numeric code when creating a new account | Integration | [ ] |
|
||||||
- Entity ID links load that entity's history inline.
|
| 11.3 | It should hide the numeric code field when editing an existing account | UI | [ ] |
|
||||||
- Snapshot link opens inspector showing full entity pull `[*]`.
|
| 11.4 | It should require a name and account type | Integration | [ ] |
|
||||||
- No pagination (all history rows shown).
|
| 11.5 | It should allow setting Invoice Allowance, Vendor Allowance, and Applicability as dropdown enums | UI | [ ] |
|
||||||
|
| 11.6 | It should show a Client Overrides grid with client typeahead and override name | UI | [ ] |
|
||||||
|
| 11.7 | It should validate that no client appears more than once in the Client Overrides grid | Unit + Integration | [ ] |
|
||||||
|
| 11.8 | It should validate that the numeric code is unique when creating a new account | Unit + Integration | [ ] |
|
||||||
|
| 11.9 | It should reindex the account and all client overrides in Solr after a successful save | Integration | [ ] |
|
||||||
|
|
||||||
**Inspector**
|
---
|
||||||
- Card showing all attributes of an entity at current database value.
|
|
||||||
- Clickable entity IDs recurse into history.
|
|
||||||
|
|
||||||
### Import Batches
|
## Vendors
|
||||||
|
|
||||||
**Grid Display**
|
### Grid Display Behaviors
|
||||||
- Columns: Date, Source, Status, User, Imported, Pre-existing, Suppressed.
|
|
||||||
- Row action: External link to transactions filtered by import batch.
|
|
||||||
|
|
||||||
**Filtering**
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Date range (start/end).
|
|---|----------|---------------|--------|
|
||||||
- Source (enum: import-source values).
|
| 12.1 | It should display a table with columns: Name, Email, Default Account | UI | [ ] |
|
||||||
|
| 12.2 | It should show an "Unused" pill in red when a vendor has 0 clients | UI | [ ] |
|
||||||
|
| 12.3 | It should show a "Used by N clients" pill in primary color when a vendor is assigned to clients | UI | [ ] |
|
||||||
|
| 12.4 | It should show a "Used N times" pill in secondary color when a vendor has transaction usage | UI | [ ] |
|
||||||
|
| 12.5 | It should show an Edit button on each row that opens the vendor wizard | UI | [ ] |
|
||||||
|
| 12.6 | It should show a "Merge" button for merging vendors | UI | [ ] |
|
||||||
|
| 12.7 | It should show a "New Vendor" button | UI | [ ] |
|
||||||
|
|
||||||
**Sorting**
|
### Filtering Behaviors
|
||||||
- By date, source, status, user.
|
|
||||||
|
|
||||||
### Excel Invoices
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 13.1 | It should filter vendors by name using case-insensitive substring match on upper-cased name | Integration | [ ] |
|
||||||
|
| 13.2 | It should filter vendors by visibility: All, Only hidden, or Only global | Integration | [ ] |
|
||||||
|
|
||||||
**Display**
|
### Vendor Wizard Behaviors
|
||||||
- Single-page form with large textarea for tab-separated invoice data.
|
|
||||||
- Shows sample data as placeholder.
|
|
||||||
|
|
||||||
**Import Behavior**
|
| # | Behavior | Test Strategy | Status |
|
||||||
- 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.
|
| 14.1 | It should show a multi-step wizard with steps: Info, Terms, Account, Address, Legal | UI | [ ] |
|
||||||
- Groups rows into: new, existing (by vendor+client+invoice-number), errors.
|
| 14.2 | It should require a name of at least 3 characters on the Info step | Unit + Integration | [ ] |
|
||||||
- For new rows: creates invoice, optionally payment (if "Cash" check), and cash transaction.
|
| 14.3 | It should allow toggling a "Print As" alias on the Info step | UI | [ ] |
|
||||||
- Cash invoices get status "paid" with zero outstanding balance.
|
| 14.4 | It should show a "Hidden" checkbox on the Info step visible only to admins | UI | [ ] |
|
||||||
- Non-cash invoices get status "unpaid" with full outstanding balance.
|
| 14.5 | It should allow setting terms in days and a grid of client-specific terms overrides on the Terms step | UI | [ ] |
|
||||||
- Results shown as pills: imported count, extant count, vendors not found (hover tooltip), error grid.
|
| 14.6 | It should allow configuring a list of clients for automatically paid when due on the Terms step | UI | [ ] |
|
||||||
|
| 14.7 | It should allow selecting a default account via typeahead on the Account step | UI | [ ] |
|
||||||
|
| 14.8 | It should show an Account Overrides grid where account typeahead is scoped by selected client | Integration | [ ] |
|
||||||
|
| 14.9 | It should allow entering address fields with a 2-character state and 5-character zip on the Address step | UI | [ ] |
|
||||||
|
| 14.10 | It should allow entering a legal entity name OR first/middle/last name, TIN, TIN type, and 1099 type on the Legal step | UI | [ ] |
|
||||||
|
| 14.11 | It should validate that terms override clients are unique with no duplicates | Unit + Integration | [ ] |
|
||||||
|
| 14.12 | It should reindex the vendor name and hidden flag in Solr after a successful save | Integration | [ ] |
|
||||||
|
|
||||||
### Sales Summaries
|
### Vendor Merge Behaviors
|
||||||
|
|
||||||
**Grid Display**
|
| # | Behavior | Test Strategy | Status |
|
||||||
- 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.
|
| 15.1 | It should open a modal with Source Vendor and Target Vendor selectors | UI | [ ] |
|
||||||
- Total row shows "Total: $X" with green pill if balanced, red if unbalanced.
|
| 15.2 | It should validate that the source and target vendors are different | Unit + Integration | [ ] |
|
||||||
- Row action: Edit button.
|
| 15.3 | It should retract all references to the source vendor and assert them as the target vendor on merge | Integration | [ ] |
|
||||||
|
| 15.4 | It should show a success notification after a successful merge | UI | [ ] |
|
||||||
|
|
||||||
**Filtering**
|
---
|
||||||
- Date range (start/end).
|
|
||||||
- Scoped to user's valid clients.
|
|
||||||
|
|
||||||
**Edit Wizard**
|
## Rules
|
||||||
- Shows all sales summary items in a grid: Category, Account, Debits, Credits.
|
|
||||||
- Manual items: editable category, account typeahead, debit/credit inputs, removable.
|
### Grid Display Behaviors
|
||||||
- Auto items: read-only category and amount, editable account.
|
|
||||||
- Account typeahead is scoped by client and filtered for invoice-purpose accounts.
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Live total row and unbalanced row update on amount changes.
|
|---|----------|---------------|--------|
|
||||||
- Validation: each item must have exactly one of credit or debit, not both.
|
| 16.1 | It should display a table with columns: Client, Bank Account, Description, Amount, Note | UI | [ ] |
|
||||||
- Validation: debits must equal credits (balanced).
|
| 16.2 | It should show a group pill in the Client column when the rule applies to a client group | UI | [ ] |
|
||||||
- Save updates ledger-mapped account assignments. Manual items get `manual?` flag and ledger-side/amount attached.
|
| 16.3 | It should show amount gte/lte filters as pills in the Amount column | UI | [ ] |
|
||||||
|
| 16.4 | It should show Delete, Execute, and Edit row action buttons | UI | [ ] |
|
||||||
|
| 16.5 | It should show a "New Transaction Rule" button | UI | [ ] |
|
||||||
|
|
||||||
|
### Filtering Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 17.1 | It should filter rules by vendor using an entity typeahead | Integration | [ ] |
|
||||||
|
| 17.2 | It should filter rules by note using case-insensitive regex match | Integration | [ ] |
|
||||||
|
| 17.3 | It should filter rules by description using case-insensitive substring match | Integration | [ ] |
|
||||||
|
| 17.4 | It should filter rules by client group using exact upper-cased match | Integration | [ ] |
|
||||||
|
|
||||||
|
### Transaction Rule Wizard Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 18.1 | It should show a two-step wizard: Edit then Test | UI | [ ] |
|
||||||
|
| 18.2 | It should require a description regex pattern of at least 3 characters on the Edit step | Unit + Integration | [ ] |
|
||||||
|
| 18.3 | It should allow toggling optional filters for Client, Client Group, Bank Account, Amount range, and Day of Month range | UI | [ ] |
|
||||||
|
| 18.4 | It should scope the bank account selector to the selected client | Integration | [ ] |
|
||||||
|
| 18.5 | It should allow assigning a vendor, configuring account grids, and setting approval status as outcomes | UI | [ ] |
|
||||||
|
| 18.6 | It should derive account location from the account's fixed location, client locations, or "Shared" | Unit | [ ] |
|
||||||
|
| 18.7 | It should validate that account percentages sum to exactly 100% | Unit + Integration | [ ] |
|
||||||
|
| 18.8 | It should validate that the selected bank account belongs to the selected client | Unit + Integration | [ ] |
|
||||||
|
| 18.9 | It should validate that the rule location matches the account's fixed location when one is set | Unit + Integration | [ ] |
|
||||||
|
| 18.10 | It should show up to 15 matching transactions on the Test step with client, bank, date, and description | Integration | [ ] |
|
||||||
|
| 18.11 | It should display a badge showing the total match count with "99+" when 99 or more transactions match | UI | [ ] |
|
||||||
|
|
||||||
|
### Rule Execution Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 19.1 | It should open a dialog with checkbox-selectable transactions that match the rule and are unapproved | UI | [ ] |
|
||||||
|
| 19.2 | It should include only transactions on or after the client's locked-until date | Integration | [ ] |
|
||||||
|
| 19.3 | It should allow selecting all matching transactions or individual transactions | UI | [ ] |
|
||||||
|
| 19.4 | It should apply rule coding to each selected transaction | Integration | [ ] |
|
||||||
|
| 19.5 | It should update the Solr index after rule execution | Integration | [ ] |
|
||||||
|
| 19.6 | It should show a notification reading "Successfully coded X of Y transactions!" after execution | UI | [ ] |
|
||||||
|
|
||||||
|
### Rule Deletion Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 20.1 | It should show a confirmation dialog before deleting a rule | UI | [ ] |
|
||||||
|
| 20.2 | It should retract the rule entity from the database on confirmation | Integration | [ ] |
|
||||||
|
| 20.3 | It should fade out the row with a "live-removed" animation after deletion | UI | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Jobs
|
||||||
|
|
||||||
|
### Grid Display Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 21.1 | It should display a table with columns: Start time, End time, Duration, Name, Status | UI | [ ] |
|
||||||
|
| 21.2 | It should show status values as running, pending, succeeded, or failed | UI | [ ] |
|
||||||
|
| 21.3 | It should display ECS tasks filtered by the INTEGREAT_JOB environment variable | Integration | [ ] |
|
||||||
|
| 21.4 | It should show a "Run job" button | UI | [ ] |
|
||||||
|
|
||||||
|
### Job Start Dialog Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 22.1 | It should show a job type dropdown with options: 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 | UI | [ ] |
|
||||||
|
| 22.2 | It should show a dynamic subform with an S3 URL path input for Bulk Journal Import and Register Invoice Import | UI | [ ] |
|
||||||
|
| 22.3 | It should show a client typeahead and days input (1-120) for Load Historical Square Sales | UI | [ ] |
|
||||||
|
| 22.4 | It should prevent starting a job that is already running | Integration | [ ] |
|
||||||
|
| 22.5 | It should launch an ECS Fargate Spot task on submit | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## History
|
||||||
|
|
||||||
|
### Search Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 23.1 | It should allow searching for an entity by numeric Datomic entity ID | UI | [ ] |
|
||||||
|
| 23.2 | It should show an error notification when the entity ID cannot be parsed as a Long | Integration | [ ] |
|
||||||
|
|
||||||
|
### Display Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 24.1 | It should display a history table with columns: Date, User, Field, From value, To value | UI | [ ] |
|
||||||
|
| 24.2 | It should format date values in local format | Unit | [ ] |
|
||||||
|
| 24.3 | It should display large integers greater than 1 million as clickable links to that entity's history | UI | [ ] |
|
||||||
|
| 24.4 | It should display nil values as "(none)" | Unit | [ ] |
|
||||||
|
| 24.5 | It should allow clicking an entity ID to load that entity's history inline | Integration | [ ] |
|
||||||
|
| 24.6 | It should show a Snapshot link that opens an inspector displaying all entity attributes | UI | [ ] |
|
||||||
|
| 24.7 | It should show all history rows without pagination | Integration | [ ] |
|
||||||
|
|
||||||
|
### Inspector Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 25.1 | It should display a card showing all attributes of an entity at the current database value | UI | [ ] |
|
||||||
|
| 25.2 | It should allow clicking entity IDs within the inspector to recurse into that entity's history | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Imports
|
||||||
|
|
||||||
|
### Grid Display Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 26.1 | It should display a table with columns: Date, Source, Status, User, Imported, Pre-existing, Suppressed | UI | [ ] |
|
||||||
|
| 26.2 | It should show an external link on each row to transactions filtered by import batch | UI | [ ] |
|
||||||
|
| 26.3 | It should show a "New Import" button | UI | [ ] |
|
||||||
|
|
||||||
|
### Filtering Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 27.1 | It should filter import batches by date range | Integration | [ ] |
|
||||||
|
| 27.2 | It should filter import batches by source | Integration | [ ] |
|
||||||
|
|
||||||
|
### Sorting Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 28.1 | It should sort import batches by date, source, status, or user | Integration | [ ] |
|
||||||
|
| 28.2 | It should paginate results with 25 import batches per page by default | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Excel Invoices
|
||||||
|
|
||||||
|
### Display Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 29.1 | It should display a single-page form with a large textarea for tab-separated invoice data | UI | [ ] |
|
||||||
|
| 29.2 | It should show sample data as a placeholder in the textarea | UI | [ ] |
|
||||||
|
|
||||||
|
### Import Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 30.1 | It should parse 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 | Unit | [ ] |
|
||||||
|
| 30.2 | It should resolve the client by code or name | Integration | [ ] |
|
||||||
|
| 30.3 | It should resolve the vendor by exact case-sensitive name match | Integration | [ ] |
|
||||||
|
| 30.4 | It should resolve the account by numeric code | Integration | [ ] |
|
||||||
|
| 30.5 | It should group rows into new, existing, and error categories | Unit | [ ] |
|
||||||
|
| 30.6 | It should create a paid invoice with zero outstanding balance and a cash transaction when the check type is "Cash" | Integration | [ ] |
|
||||||
|
| 30.7 | It should create an unpaid invoice with full outstanding balance when the check type is not "Cash" | Integration | [ ] |
|
||||||
|
| 30.8 | It should display results as pills showing imported count, extant count, and vendors not found with hover tooltip | UI | [ ] |
|
||||||
|
| 30.9 | It should display an error grid for rows that failed validation | UI | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sales Summaries
|
||||||
|
|
||||||
|
### Grid Display Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 31.1 | It should display a table with columns: Client, Date, Debits, Credits | UI | [ ] |
|
||||||
|
| 31.2 | It should hide the Client column when only one client is selected | UI | [ ] |
|
||||||
|
| 31.3 | It should show each debit/credit item with category, amount, and a red "missing account" pill when no account is mapped | UI | [ ] |
|
||||||
|
| 31.4 | It should show a total row with a green pill when debits equal credits, or a red pill when unbalanced | UI | [ ] |
|
||||||
|
| 31.5 | It should show an Edit button on each row | UI | [ ] |
|
||||||
|
|
||||||
|
### Filtering Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 32.1 | It should filter sales summaries by date range | Integration | [ ] |
|
||||||
|
| 32.2 | It should scope results to the user's valid clients | Integration | [ ] |
|
||||||
|
|
||||||
|
### Edit Wizard Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 33.1 | It should display a grid of sales summary items with Category, Account, Debits, and Credits columns | UI | [ ] |
|
||||||
|
| 33.2 | It should allow editing the category, account, and debit/credit amounts for manual items | UI | [ ] |
|
||||||
|
| 33.3 | It should allow removing manual items from the grid | UI | [ ] |
|
||||||
|
| 33.4 | It should display auto-generated items with read-only category and amount but editable account | UI | [ ] |
|
||||||
|
| 33.5 | It should scope the account typeahead to the client and filter for invoice-purpose accounts | Integration | [ ] |
|
||||||
|
| 33.6 | It should update the live total row and unbalanced row when amounts change | UI | [ ] |
|
||||||
|
| 33.7 | It should validate that each item has exactly one of credit or debit, not both | Unit + Integration | [ ] |
|
||||||
|
| 33.8 | It should validate that total debits equal total credits before saving | Unit + Integration | [ ] |
|
||||||
|
| 33.9 | It should update ledger-mapped account assignments and flag manual items on save | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Cross-Cutting Behaviors
|
## Cross-Cutting Behaviors
|
||||||
|
|
||||||
### Admin-only Access
|
### Admin-Only Access Behaviors
|
||||||
- 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
|
| # | Behavior | Test Strategy | Status |
|
||||||
- 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.
|
| 34.1 | It should redirect unauthenticated users to the login page on all admin routes | Integration | [ ] |
|
||||||
|
| 34.2 | It should show an authorization failure for authenticated non-admin users on all admin routes | Integration | [ ] |
|
||||||
|
| 34.3 | It should require admin role for all mutating admin handlers | Integration | [ ] |
|
||||||
|
|
||||||
### History Tracking
|
### Audit History Behaviors
|
||||||
- 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
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Admin role is required for all admin handlers.
|
|---|----------|---------------|--------|
|
||||||
- No finer-grained permissions within admin area.
|
| 35.1 | It should record the admin user who performed each mutating operation via the `:audit/user` attribute | Integration | [ ] |
|
||||||
|
| 35.2 | It should write all mutating operations through `audit-transact` or `audit-transact-batch` | Integration | [ ] |
|
||||||
|
| 35.3 | It should allow querying all changes to an entity from Datomic's history database on the History page | Integration | [ ] |
|
||||||
|
|
||||||
### Form Validation Patterns
|
### Impersonation Behaviors
|
||||||
- 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
|
| # | Behavior | Test Strategy | Status |
|
||||||
- After create/update of clients, accounts, and vendors, Solr documents are reindexed.
|
|---|----------|---------------|--------|
|
||||||
- This affects search typeaheads across the application.
|
| 36.1 | It should allow admin users to select a client from the global client selector to filter admin grids | UI | [ ] |
|
||||||
|
| 36.2 | It should respect the selected client when filtering the Clients, Transaction Rules, and Sales Summaries grids | Integration | [ ] |
|
||||||
|
|
||||||
## Edge Cases
|
### Form Validation Behaviors
|
||||||
|
|
||||||
**Client Management**
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Client code must be unique on create; immutable on edit.
|
|---|----------|---------------|--------|
|
||||||
- Bank account code is immutable after creation.
|
| 37.1 | It should enforce form structure via Malli schemas | Unit | [ ] |
|
||||||
- Financial code (numeric) required when "Include in Reports" is true.
|
| 37.2 | It should validate query params, route params, and form params via `wrap-schema-enforce` | Integration | [ ] |
|
||||||
- Square location refresh times out after 2 seconds → shows "No locations found."
|
| 37.3 | It should re-render dialogs with field-level validation errors on 400 responses | Integration | [ ] |
|
||||||
- Bank accounts are sortable via drag-and-drop (sortable.js integration via `hx-trigger="end"`).
|
|
||||||
|
|
||||||
**Account Management**
|
### Solr Indexing Behaviors
|
||||||
- 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**
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Vendor merge fails if source equals target.
|
|---|----------|---------------|--------|
|
||||||
- Account override typeahead depends on client selection; changing client updates available accounts.
|
| 38.1 | It should reindex Solr documents after creating or updating a client | Integration | [ ] |
|
||||||
- Terms override requires unique clients (no duplicate client overrides).
|
| 38.2 | It should reindex Solr documents after creating or updating a vendor or account | Integration | [ ] |
|
||||||
|
|
||||||
**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
|
## Test Data Requirements
|
||||||
|
|
||||||
### Users
|
| Entity | Requirements |
|
||||||
- Admin user with `:user/role :user-role/admin`.
|
|--------|-------------|
|
||||||
- Non-admin user with `:user/role :user-role/user` for access control tests.
|
| **Users** | Admin user with `:user/role :user-role/admin`; non-admin user with `:user/role :user-role/user` |
|
||||||
|
| **Clients** | Client with name, code, and locations; client with locked-until in past and future; client with bank accounts (cash, credit, checking); client with Square auth token; client with feature flags and groups |
|
||||||
|
| **Accounts** | Account with unique numeric code, name, and type; account with fixed location; account with client overrides |
|
||||||
|
| **Vendors** | Vendor with name and 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 |
|
||||||
|
| **Import Batches** | Import batch entities with date, source, and status |
|
||||||
|
| **Sales Summaries** | Sales summary with ledger-mapped items; both manual and auto-generated items |
|
||||||
|
|
||||||
### Clients
|
## Existing Tests to Preserve
|
||||||
- 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
|
- `test/clj/auto_ap/integration/graphql/users.clj` — User role and permission tests
|
||||||
- Account with `:account/numeric-code` (unique), `:account/name`, `:account/type`.
|
- `test/clj/auto_ap/integration/graphql/accounts.clj` — Account search and override tests
|
||||||
- Account with `:account/location` fixed.
|
- `test/clj/auto_ap/integration/graphql/vendors.clj` — Vendor management tests
|
||||||
- Account with client overrides.
|
- `test/clj/auto_ap/integration/graphql/transaction_rules.clj` — Rule matching and execution tests
|
||||||
|
|
||||||
### 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
|
## Dependencies
|
||||||
|
|
||||||
- Datomic (history, transactions, pull API)
|
- Datomic (primary store, history, pull API)
|
||||||
- HTMX (frontend interactivity)
|
- HTMX (frontend interactivity)
|
||||||
- Alpine.js (modal state, conditional visibility)
|
- Alpine.js (modal state, conditional visibility)
|
||||||
- Chartist (dashboard charts)
|
- Chartist (dashboard charts)
|
||||||
@@ -388,10 +492,3 @@ All routes are wrapped with `wrap-client-redirect-unauthenticated` followed by `
|
|||||||
- ECS API (background jobs)
|
- ECS API (background jobs)
|
||||||
- Malli (schema validation)
|
- Malli (schema validation)
|
||||||
- Bidi (routing)
|
- 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
|
|
||||||
|
|||||||
@@ -4,163 +4,181 @@
|
|||||||
|
|
||||||
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.
|
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
|
**Testing Philosophy**
|
||||||
|
- Prefer unit tests for pure business logic (JWT generation, compression, validation)
|
||||||
|
- Use integration tests for database interactions and cross-system flows (OAuth callback, session management)
|
||||||
|
- Use UI tests only for end-to-end happy paths that touch multiple pages
|
||||||
|
- Every behavior must be user-visible; no tests for implementation details
|
||||||
|
|
||||||
| Route | Method | Handler | 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
|
## Testing Patterns
|
||||||
|
|
||||||
### Unit Tests
|
### Pattern: OAuth Login Flow
|
||||||
|
The standard authentication flow follows these steps:
|
||||||
|
1. Unauthenticated user navigates to a protected page
|
||||||
|
2. User is redirected to `/login`
|
||||||
|
3. User clicks "Sign in with Google"
|
||||||
|
4. Browser redirects to Google OAuth consent screen
|
||||||
|
5. User consents and Google redirects to `/api/oauth?code=<code>&state=<state>`
|
||||||
|
6. Server exchanges code for token, fetches profile, finds/creates user
|
||||||
|
7. Server redirects to original page (or `/`) with `?jwt=<token>`
|
||||||
|
8. Client reads JWT and establishes session
|
||||||
|
|
||||||
- `user->jwt` returns nil when user or oauth-token is nil
|
**Test implications:** Integration test the OAuth callback handler. UI test only the happy path once.
|
||||||
- `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
|
### Pattern: Middleware Stack
|
||||||
|
Every protected route passes through authentication middleware:
|
||||||
|
1. `wrap-session-version` — validates session version, redirects to login if outdated
|
||||||
|
2. `wrap-secure` — checks for authenticated session, redirects to login if missing
|
||||||
|
3. `wrap-admin` — checks for admin role, redirects to login if not admin
|
||||||
|
4. `wrap-client-redirect-unauthenticated` — converts 401 responses to HTMX redirects
|
||||||
|
|
||||||
- `GET /api/oauth` with valid Google `code` exchanges code for access token
|
**Test implications:** Integration test each middleware independently. UI tests only verify redirect behavior.
|
||||||
- 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)
|
### Pattern: JWT Claims
|
||||||
|
The JWT token contains user identity and permissions:
|
||||||
|
1. `:user`, `:exp`, `:db/id`, `:user/role`, `:user/name` for all users
|
||||||
|
2. `:gz-clients` (compressed) for admin and read-only users
|
||||||
|
3. `:user/clients` (plain) for regular users
|
||||||
|
|
||||||
#### Happy Path: OAuth Login
|
**Test implications:** Unit test JWT generation for each role type.
|
||||||
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)
|
## Login
|
||||||
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
|
### Display Behaviors
|
||||||
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
|
| # | Behavior | Test Strategy | Status |
|
||||||
1. Authenticated user's session expires
|
|---|----------|---------------|--------|
|
||||||
2. HTMX request fires (e.g., card refresh)
|
| 1.1 | It should display a "Sign in with Google" button on the login page | UI | [ ] |
|
||||||
3. `wrap-secure` detects missing authentication
|
|
||||||
4. Response includes `hx-redirect: /login`
|
|
||||||
5. HTMX redirects the browser to login page
|
|
||||||
|
|
||||||
#### Session Version Mismatch
|
### OAuth Behaviors
|
||||||
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
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 1.2 | It should redirect to Google OAuth when the user clicks "Sign in with Google" | UI | [ ] |
|
||||||
|
| 1.3 | It should exchange the authorization code for an access token on callback | Integration | [ ] |
|
||||||
|
| 1.4 | It should fetch the user's Google profile using the access token | Integration | [ ] |
|
||||||
|
| 1.5 | It should create a new user account when the user logs in for the first time | Integration | [ ] |
|
||||||
|
| 1.6 | It should find the existing user account on subsequent logins | Integration | [ ] |
|
||||||
|
| 1.7 | It should redirect to the original page after successful OAuth | Integration | [ ] |
|
||||||
|
| 1.8 | It should redirect to the root page when no return URL is provided | Integration | [ ] |
|
||||||
|
| 1.9 | It should establish a server-side session with user identity and version | Integration | [ ] |
|
||||||
|
| 1.10 | It should pass the JWT token in the query string after successful OAuth | Integration | [ ] |
|
||||||
|
| 1.11 | It should display the user's clients and data after successful login | UI | [ ] |
|
||||||
|
| 1.12 | It should handle users without email via Google provider ID | Integration | [ ] |
|
||||||
|
| 1.13 | It should return 401 with error message when the OAuth code is missing | Integration | [ ] |
|
||||||
|
| 1.14 | It should return 401 when the OAuth code is invalid or expired | Integration | [ ] |
|
||||||
|
| 1.15 | It should return 401 and log a warning when the Google network request fails | Integration | [ ] |
|
||||||
|
|
||||||
### 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
|
## Logout
|
||||||
- **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
|
| # | Behavior | Test Strategy | Status |
|
||||||
- **Logout without active session**: Still clears session and redirects to login
|
|---|----------|---------------|--------|
|
||||||
- **Double logout**: Idempotent; session remains empty
|
| 2.1 | It should clear the session when the user navigates to `/logout` | Integration | [ ] |
|
||||||
|
| 2.2 | It should redirect to the login page after logout | Integration | [ ] |
|
||||||
|
| 2.3 | It should remain idempotent when logging out without an active session | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Impersonation
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 3.1 | It should allow admin users to assume another user's identity via signed JWT | Integration | [ ] |
|
||||||
|
| 3.2 | It should validate the impersonation JWT signature with `:jwt-secret` and `:hs512` | Integration | [ ] |
|
||||||
|
| 3.3 | It should reject expired impersonation JWTs | Integration | [ ] |
|
||||||
|
| 3.4 | It should block non-admin users from accessing `/impersonate` | Integration | [ ] |
|
||||||
|
| 3.5 | It should block unauthenticated users from accessing `/impersonate` | Integration | [ ] |
|
||||||
|
| 3.6 | It should replace the admin's session with the impersonated user's session | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Session Management
|
||||||
|
|
||||||
|
### Authentication Gate Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 4.1 | It should allow authenticated requests to proceed to protected routes | Integration | [ ] |
|
||||||
|
| 4.2 | It should redirect unauthenticated users to `/login` with a `redirect-to` parameter | Integration | [ ] |
|
||||||
|
| 4.3 | It should return `hx-redirect: /login` for unauthenticated HTMX requests | Integration | [ ] |
|
||||||
|
|
||||||
|
### Admin Gate Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 5.1 | It should allow admin requests to proceed to admin-only routes | Integration | [ ] |
|
||||||
|
| 5.2 | It should redirect non-admin users to `/login` when accessing admin routes | Integration | [ ] |
|
||||||
|
|
||||||
|
### Session Version Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 6.1 | It should invalidate sessions with outdated version numbers | Integration | [ ] |
|
||||||
|
| 6.2 | It should redirect to `/login` when an outdated session accesses normal routes | Integration | [ ] |
|
||||||
|
| 6.3 | It should return `hx-redirect: /login` for outdated sessions on HTMX routes | Integration | [ ] |
|
||||||
|
| 6.4 | It should return 401 for outdated sessions on GraphQL routes | Integration | [ ] |
|
||||||
|
| 6.5 | It should treat sessions without a version as outdated | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-Cutting Behaviors
|
||||||
|
|
||||||
|
### JWT Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 7.1 | It should generate a JWT containing the user's role and client access on login | Unit | [ ] |
|
||||||
|
| 7.2 | It should compress the client list for admin users to fit in the JWT | Unit | [ ] |
|
||||||
|
| 7.3 | It should compress the client list for read-only users to fit in the JWT | Unit | [ ] |
|
||||||
|
| 7.4 | It should include a plain client list for regular users in the JWT | Unit | [ ] |
|
||||||
|
| 7.5 | It should create API tokens with admin role and 1000-day expiration | Unit | [ ] |
|
||||||
|
|
||||||
|
### Middleware Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 8.1 | It should convert 401 responses to HTMX redirects for unauthenticated users | Integration | [ ] |
|
||||||
|
|
||||||
|
### Role-Based Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 9.1 | It should allow admin users to access all clients | Integration | [ ] |
|
||||||
|
| 9.2 | It should allow regular users to access only their assigned clients | Integration | [ ] |
|
||||||
|
| 9.3 | It should allow read-only users to access all clients with view-only permissions | Integration | [ ] |
|
||||||
|
| 9.4 | It should handle admin users with no clients by providing an empty compressed list | Unit | [ ] |
|
||||||
|
| 9.5 | It should handle regular users with no clients by providing an empty client vector | Unit | [ ] |
|
||||||
|
|
||||||
|
### Security Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 10.1 | It should reject tampered JWTs during impersonation | Integration | [ ] |
|
||||||
|
| 10.2 | It should treat sessions with nil identity as unauthenticated | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Test Data Requirements
|
## Test Data Requirements
|
||||||
|
|
||||||
### Users
|
| Entity | Requirements |
|
||||||
- User with `:user/role :user.role/admin`, multiple clients
|
|--------|-------------|
|
||||||
- User with `:user/role :user.role/user`, subset of clients
|
| **Users** | Admin user with multiple clients; regular user with subset of clients; read-only user with multiple clients; new user (not in database) with Google provider details; existing user with Google provider details |
|
||||||
- User with `:user/role :user.role/read-only`, multiple clients
|
| **Clients** | Multiple clients with `:client/code`, `:client/name`, `:client/locations`; client associations on users |
|
||||||
- New user (not yet in database) with Google provider details
|
| **OAuth Mock** | Mock Google token endpoint responses (success and failure); mock Google userinfo endpoint responses |
|
||||||
- Existing user with Google provider details
|
|
||||||
|
|
||||||
### Clients
|
## Existing Tests to Preserve
|
||||||
- Multiple clients with `:client/code`, `:client/name`, `:client/locations`
|
|
||||||
- Client associations on users
|
|
||||||
|
|
||||||
### OAuth Mock
|
- None identified
|
||||||
- Mock Google token endpoint responses (success and failure)
|
|
||||||
- Mock Google userinfo endpoint responses
|
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
### External Services
|
- Google OAuth 2.0 (authorization code exchange and userinfo retrieval)
|
||||||
- **Google OAuth 2.0**: Authorization code exchange and userinfo retrieval
|
- Buddy JWT (token signing/unsigning with HS512)
|
||||||
- **Buddy (JWT)**: Token signing/unsigning with HS512
|
- Datomic (user lookup and creation)
|
||||||
- **Datomic**: User lookup and creation via `users/find-or-insert!`
|
- Legacy SPA (login and needs-activation pages, no SSR tests)
|
||||||
|
|
||||||
### 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
|
|
||||||
|
|||||||
@@ -4,338 +4,317 @@
|
|||||||
|
|
||||||
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.
|
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
|
**Testing Philosophy**
|
||||||
|
- Prefer unit tests for pure business logic (calculations, validations, transformations)
|
||||||
|
- Use integration tests for database interactions and cross-system flows
|
||||||
|
- Use UI tests only for end-to-end happy paths that touch multiple pages
|
||||||
|
- Every behavior must be user-visible; no tests for implementation details
|
||||||
|
|
||||||
| Route | 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
|
## Testing Patterns
|
||||||
|
|
||||||
- `/company/1099/table` - 1099 vendor data grid (HTMX)
|
### Pattern: Grid Page Behaviors
|
||||||
- `/company/1099/vendor-dialog/:vendor-id` - Edit vendor 1099 info modal
|
Most list pages in Integreat follow the same pattern:
|
||||||
- `/company/1099/vendor-dialog/:vendor-id` (POST) - Save vendor 1099 info
|
1. Fetch IDs via Datomic query with filters
|
||||||
- `/company/reports/table` - Reports data grid (HTMX)
|
2. Hydrate results via `pull-many`
|
||||||
- `/company/reports/expense/card` - Expense breakdown chart (HTMX)
|
3. Render table with sortable columns
|
||||||
- `/company/reports/expense/invoice-total-card` - Invoice totals grid (HTMX)
|
4. Support selection (individual / all / all-filtered)
|
||||||
- `/company/reports/reconciliation/card` - Reconciliation data (HTMX)
|
5. Action buttons appear conditionally based on permissions and selection state
|
||||||
- `/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
|
**Test implications:** Validate the query generation, not the rendering. UI tests only need to verify one filter and one sort work end-to-end.
|
||||||
|
|
||||||
### Company Profile
|
### Pattern: Permission Gates
|
||||||
|
Every mutating operation checks:
|
||||||
|
1. `assert-can-see-client` — user has access to the client
|
||||||
|
2. `can?` — user has the specific permission for the activity
|
||||||
|
3. Admin role checks for destructive operations
|
||||||
|
|
||||||
**Route:** `/company`
|
**Test implications:** Integration test each gate independently. UI tests only verify the happy path with a permitted user.
|
||||||
|
|
||||||
#### Client Selection State
|
### Pattern: HTMX Refresh on Client Switch
|
||||||
- **If no client is selected:** Displays a "Please select a company" placeholder card instead of profile content.
|
All company pages listen for `clientSelected from:body` event and refresh `#app-contents` with a 300ms swap animation.
|
||||||
- **If a client is selected:** Displays the company profile with name, address, downloads, and (if permitted) signature section.
|
|
||||||
|
|
||||||
#### Profile Content
|
**Test implications:** Integration test the event handling and route params. UI test only one page to verify the swap animation.
|
||||||
- **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
|
## Company Profile
|
||||||
- **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
|
### Display Behaviors
|
||||||
|
|
||||||
**Route:** `/company/1099`
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 1.1 | It should display a "Please select a company" placeholder card when no client is selected | UI | [ ] |
|
||||||
|
| 1.2 | It should display the company name as a heading when a client is selected | UI | [ ] |
|
||||||
|
| 1.3 | It should display the company address (street, city, state, zip) when address data exists | UI | [ ] |
|
||||||
|
| 1.4 | It should omit missing address fields without showing error placeholders | UI | [ ] |
|
||||||
|
| 1.5 | It should show a "Download vendor list" button | UI | [ ] |
|
||||||
|
| 1.6 | It should download a CSV/Excel export when the download button is clicked | Integration | [ ] |
|
||||||
|
|
||||||
#### Vendor Grid
|
### Signature Behaviors
|
||||||
- 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
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Opens in a modal via HTMX GET.
|
|---|----------|---------------|--------|
|
||||||
- **Address Section:** Street 1, Street 2, City, State, ZIP. ZIP validated as 5 digits or empty.
|
| 2.1 | It should show the signature section only when the user has signature edit permission | Integration | [ ] |
|
||||||
- **Legal Entity Section:**
|
| 2.2 | It should display the saved signature image when one exists | UI | [ ] |
|
||||||
- Legal Entity Name (full width) — OR —
|
| 2.3 | It should show a "New signature" button that enables drawing mode on a canvas | UI | [ ] |
|
||||||
- First Name, Middle Name, Last Name (2-col layout each)
|
| 2.4 | It should show a "Clear" button that clears the canvas while in drawing mode | UI | [ ] |
|
||||||
- **TIN Section:**
|
| 2.5 | It should show an "Accept" button that submits the drawn signature | UI | [ ] |
|
||||||
- TIN input
|
| 2.6 | It should reject invalid signature image data with a validation error | Unit + Integration | [ ] |
|
||||||
- TIN Type dropdown: EIN / SSN
|
| 2.7 | It should provide a drag-and-drop zone for uploading JPEG signature files | UI | [ ] |
|
||||||
- 1099 Type dropdown: populated from `legal-entity-1099-type` enum
|
| 2.8 | It should change the drop zone background color on hover | UI | [ ] |
|
||||||
- **Save Behavior:**
|
| 2.9 | It should refresh the signature section with the uploaded image on successful upload | Integration | [ ] |
|
||||||
- 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
|
## 1099 Reports
|
||||||
|
|
||||||
**Route:** `/company/reports/expense`
|
### Display Behaviors
|
||||||
|
|
||||||
#### Expense Breakdown Chart
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Bar chart showing expenses grouped by **top 20 expense accounts** over the **last 8 weeks**.
|
|---|----------|---------------|--------|
|
||||||
- X-axis: Week ranges (Monday-Sunday) formatted as dates.
|
| 3.1 | It should display vendors who received $600 or more in check payments during the current tax year | Integration | [ ] |
|
||||||
- Data sourced from non-voided invoices with expense account allocations.
|
| 3.2 | It should show grid columns: Client, Vendor Name, TIN, Expense Account, Address, Paid | UI | [ ] |
|
||||||
- **Filters:**
|
| 3.3 | It should display the vendor's legal entity name as a subtitle under the vendor name | UI | [ ] |
|
||||||
- Vendor typeahead (filters to specific vendor's invoices)
|
| 3.4 | It should show a 1099 type pill badge when a 1099 type is set | UI | [ ] |
|
||||||
- Account typeahead (filters to specific expense account)
|
| 3.5 | It should display the TIN with a TIN type pill (EIN or SSN) | UI | [ ] |
|
||||||
- Filters trigger HTMX GET to `/company/reports/expense/card` with `change` event.
|
| 3.6 | It should show "No address" placeholder when the vendor has no address | UI | [ ] |
|
||||||
- Chart rendered via Chart.js on canvas element with Alpine.js init.
|
| 3.7 | It should display the total paid amount as a pill badge rounded to the nearest dollar | UI | [ ] |
|
||||||
|
| 3.8 | It should show an edit icon button on each row | UI | [ ] |
|
||||||
|
| 3.9 | It should show vendors shared across multiple clients in each client's context | Integration | [ ] |
|
||||||
|
| 3.10 | It should show an empty grid when no vendors received $600+ in checks during the tax year | UI | [ ] |
|
||||||
|
|
||||||
#### Invoice Totals by Vendor
|
### Filtering & Sorting Behaviors
|
||||||
- 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
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 4.1 | It should support standard grid query params (sort, pagination, search) | Integration | [ ] |
|
||||||
|
| 4.2 | It should default sort by client code then amount | Integration | [ ] |
|
||||||
|
|
||||||
**Route:** `/company/reports/reconciliation`
|
### Edit Behaviors
|
||||||
|
|
||||||
#### Access Control
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Navigation link only visible if user has `{:subject :reconciliation-report}` permission.
|
|---|----------|---------------|--------|
|
||||||
|
| 5.1 | It should open a vendor edit dialog in a modal when the edit icon is clicked | UI | [ ] |
|
||||||
|
| 5.2 | It should display address fields (Street 1, Street 2, City, State, ZIP) in the dialog | UI | [ ] |
|
||||||
|
| 5.3 | It should validate the ZIP code as 5 digits or empty | Unit + Integration | [ ] |
|
||||||
|
| 5.4 | It should allow entering either a legal entity name or first/middle/last name | UI | [ ] |
|
||||||
|
| 5.5 | It should allow entering a TIN and selecting TIN type (EIN or SSN) | UI | [ ] |
|
||||||
|
| 5.6 | It should allow selecting a 1099 type from a dropdown | UI | [ ] |
|
||||||
|
| 5.7 | It should close the modal and refresh the row with a flash highlight on successful save | Integration | [ ] |
|
||||||
|
| 5.8 | It should null the address if all address fields are empty and no existing address | Integration | [ ] |
|
||||||
|
|
||||||
#### 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
|
## Expense Reports
|
||||||
- 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
|
### Chart Behaviors
|
||||||
|
|
||||||
**Route:** `/company/plaid`
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 6.1 | It should display a bar chart of expenses grouped by top 20 expense accounts over the last 8 weeks | UI | [ ] |
|
||||||
|
| 6.2 | It should show week ranges (Monday-Sunday) formatted as dates on the X-axis | UI | [ ] |
|
||||||
|
| 6.3 | It should provide a vendor typeahead to filter expenses to a specific vendor | Integration | [ ] |
|
||||||
|
| 6.4 | It should provide an expense account typeahead to filter to a specific account | Integration | [ ] |
|
||||||
|
| 6.5 | It should refresh the chart when filters change | Integration | [ ] |
|
||||||
|
| 6.6 | It should default to last 65 days of data but display last 8 weeks | Integration | [ ] |
|
||||||
|
|
||||||
#### Account Grid
|
### Invoice Totals Behaviors
|
||||||
- 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
|
| # | Behavior | Test Strategy | Status |
|
||||||
- "Link [client-code] account" button opens Plaid Link modal.
|
|---|----------|---------------|--------|
|
||||||
- Button only appears when a client is selected.
|
| 7.1 | It should display a grid of total invoice amounts per vendor per company | UI | [ ] |
|
||||||
- On successful Plaid Link, HTMX POSTs public token to `/company/plaid/link`.
|
| 7.2 | It should provide start and end date range filters | UI | [ ] |
|
||||||
- **Link Handler:**
|
| 7.3 | It should default the date range to the last 30 days | Integration | [ ] |
|
||||||
- Exchanges public token for access token.
|
| 7.4 | It should show the vendor name in a sticky left column | UI | [ ] |
|
||||||
- Fetches accounts from Plaid.
|
| 7.5 | It should show "-" for zero amounts | UI | [ ] |
|
||||||
- Creates `plaid-item` entity with client reference, external ID, access token, status, and timestamp.
|
| 7.6 | It should push filter changes to browser history | Integration | [ ] |
|
||||||
- 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
|
## Reconciliation Reports
|
||||||
|
|
||||||
**Route:** `/company/yodlee`
|
### Access Behaviors
|
||||||
|
|
||||||
#### Account Grid
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Displays Yodlee provider accounts for selected clients.
|
|---|----------|---------------|--------|
|
||||||
- Columns:
|
| 8.1 | It should show the reconciliation navigation link only when the user has reconciliation report permission | Integration | [ ] |
|
||||||
- **Client:** Client code (hidden if user has only one client)
|
| 8.2 | It should require start and end dates to be submitted via a "Run" button | UI | [ ] |
|
||||||
- **Provider Account:** Yodlee provider account ID
|
| 8.3 | It should show a "Please choose a time range to run the report" message when no dates are selected | UI | [ ] |
|
||||||
- **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
|
### Display Behaviors
|
||||||
- "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
|
| # | Behavior | Test Strategy | Status |
|
||||||
- "Reauthenticate" button per row opens Fastlink in edit mode for the specific provider account.
|
|---|----------|---------------|--------|
|
||||||
|
| 8.4 | It should display a grid with columns: Bank Account, Source Count, Synced Count, Approved, Unapproved, Requires Feedback, Missing | UI | [ ] |
|
||||||
|
| 8.5 | It should highlight rows with green background when external count equals synced count | UI | [ ] |
|
||||||
|
| 8.6 | It should highlight rows with red background when counts mismatch | UI | [ ] |
|
||||||
|
| 8.7 | It should display a missing transactions count with a tooltip button | UI | [ ] |
|
||||||
|
| 8.8 | It should show a popup table of missing transaction dates and amounts when the tooltip is clicked | UI | [ ] |
|
||||||
|
| 8.9 | It should hide the missing transactions tooltip when the count is zero | UI | [ ] |
|
||||||
|
|
||||||
#### Refresh (Admin Only)
|
---
|
||||||
- Admin users see a refresh icon button on each row.
|
|
||||||
- Triggers Yodlee account refresh and refreshes the row.
|
|
||||||
|
|
||||||
### Reports List
|
## Plaid Bank Linking
|
||||||
|
|
||||||
**Route:** `/company/reports`
|
### Account Grid Behaviors
|
||||||
|
|
||||||
#### Grid
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Lists previously generated reports.
|
|---|----------|---------------|--------|
|
||||||
- Columns:
|
| 9.1 | It should display a grid of Plaid-linked accounts with columns: Plaid Item, Integreat Status, Plaid Bank Status, Accounts | UI | [ ] |
|
||||||
- **Name:** Report name
|
| 9.2 | It should show a red pill with error message tooltip when any linked bank account has failed or unauthorized status | UI | [ ] |
|
||||||
- **Created by:** Creator name as pill badge
|
| 9.3 | It should show a green "Success" pill when all accounts are healthy | UI | [ ] |
|
||||||
- **Created:** Formatted creation date
|
| 9.4 | It should display linked accounts with name, masked number, last synced date, and identicon | UI | [ ] |
|
||||||
- Row actions:
|
| 9.5 | It should support sorting by external ID and Plaid bank status | Integration | [ ] |
|
||||||
- **Download:** Link to S3-hosted report file
|
| 9.6 | It should show an empty grid when no bank accounts are linked | UI | [ ] |
|
||||||
- **Delete:** Admin-only trash icon. Deletes S3 object and retracts entity.
|
|
||||||
- Supports filtering by date range and client.
|
### Link Behaviors
|
||||||
- Supports sorting by client, created, creator, name.
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 10.1 | It should show a "Link account" button when a client is selected | UI | [ ] |
|
||||||
|
| 10.2 | It should hide the link button when no client is selected | UI | [ ] |
|
||||||
|
| 10.3 | It should open a Plaid Link modal when the link button is clicked | UI | [ ] |
|
||||||
|
| 10.4 | It should create the Plaid item and accounts in the system after successful linking | Integration | [ ] |
|
||||||
|
| 10.5 | It should redirect back to the Plaid page after successful account linking | Integration | [ ] |
|
||||||
|
|
||||||
|
### Re-authenticate Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 11.1 | It should show a "Reauthenticate" button on each row | UI | [ ] |
|
||||||
|
| 11.2 | It should open Plaid Link in update mode when reauthenticate is clicked | UI | [ ] |
|
||||||
|
| 11.3 | It should refresh the row after successful reauthentication | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Yodlee Bank Linking
|
||||||
|
|
||||||
|
### Account Grid Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 12.1 | It should display a grid of Yodlee provider accounts with columns: Client, Provider Account, Status, Detailed Status, Last Updated, Accounts | UI | [ ] |
|
||||||
|
| 12.2 | It should hide the Client column when the user has only one client | UI | [ ] |
|
||||||
|
| 12.3 | It should show a green pill for success status and a yellow pill for other statuses | UI | [ ] |
|
||||||
|
| 12.4 | It should display linked accounts with name and number | UI | [ ] |
|
||||||
|
| 12.5 | It should support sorting by status, client, provider account, and last updated | Integration | [ ] |
|
||||||
|
| 12.6 | It should show an empty grid when no bank accounts are linked | UI | [ ] |
|
||||||
|
|
||||||
|
### Link Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 13.1 | It should show a "Link new account" button | UI | [ ] |
|
||||||
|
| 13.2 | It should disable the link button and show helper text when no client is selected | UI | [ ] |
|
||||||
|
| 13.3 | It should open a Yodlee Fastlink modal when the link button is clicked | UI | [ ] |
|
||||||
|
| 13.4 | It should display an error notification and close the modal after 3 seconds when Yodlee returns an error | Integration | [ ] |
|
||||||
|
|
||||||
|
### Re-authenticate Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 14.1 | It should show a "Reauthenticate" button per row | UI | [ ] |
|
||||||
|
| 14.2 | It should open Fastlink in edit mode when reauthenticate is clicked | UI | [ ] |
|
||||||
|
| 14.3 | It should refresh the row after successful reauthentication | Integration | [ ] |
|
||||||
|
|
||||||
|
### Admin Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 15.1 | It should show a refresh button on each row for admin users | Integration | [ ] |
|
||||||
|
| 15.2 | It should trigger a Yodlee account refresh when the refresh button is clicked | Integration | [ ] |
|
||||||
|
| 15.3 | It should refresh the row after successful Yodlee refresh | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Generated Reports List
|
||||||
|
|
||||||
|
### Display Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 16.1 | It should display a grid of previously generated reports with columns: Name, Created by, Created | UI | [ ] |
|
||||||
|
| 16.2 | It should show the creator name as a pill badge | UI | [ ] |
|
||||||
|
| 16.3 | It should show an empty grid when no reports have been generated | UI | [ ] |
|
||||||
|
|
||||||
|
### Row Action Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 17.1 | It should provide a download link to the report file on each row | UI | [ ] |
|
||||||
|
| 17.2 | It should show a delete button on each row for admin users | Integration | [ ] |
|
||||||
|
| 17.3 | It should delete the report and its file when the delete button is clicked | Integration | [ ] |
|
||||||
|
|
||||||
|
### Filtering & Sorting Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 18.1 | It should support filtering by date range and client | Integration | [ ] |
|
||||||
|
| 18.2 | It should support sorting by client, created date, creator, and name | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Cross-Cutting Behaviors
|
## Cross-Cutting Behaviors
|
||||||
|
|
||||||
### Client Switching
|
### Client Switching Behaviors
|
||||||
- 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
|
| # | Behavior | Test Strategy | Status |
|
||||||
- **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.
|
| 19.1 | It should refresh page content with a 300ms swap animation when the user switches clients | Integration | [ ] |
|
||||||
|
| 19.2 | It should show appropriate placeholder states when no client is selected on pages that require one | UI | [ ] |
|
||||||
|
| 19.3 | It should operate 1099 and reports grids across all visible clients when no single client is selected | Integration | [ ] |
|
||||||
|
|
||||||
### Permissions
|
### Permission Behaviors
|
||||||
|
|
||||||
| Feature | Required Permission |
|
| # | Behavior | Test Strategy | Status |
|
||||||
|---------|-------------------|
|
|---|----------|---------------|--------|
|
||||||
| All company pages | Authenticated user with client access |
|
| 20.1 | It should block access to company pages for unauthenticated users | Integration | [ ] |
|
||||||
| Signature upload | `{:subject :signature :activity :edit}` |
|
| 20.2 | It should block access to company pages for users without client access | Integration | [ ] |
|
||||||
| Bank Sync Report nav | `{:subject :reconciliation-report}` |
|
| 20.3 | It should hide the signature section from users without signature edit permission | Integration | [ ] |
|
||||||
| Delete reports | Admin role |
|
| 20.4 | It should hide the reconciliation report navigation link from users without reconciliation report permission | Integration | [ ] |
|
||||||
| Yodlee refresh | Admin role (`is-admin?`) |
|
| 20.5 | It should hide the delete report button from non-admin users | Integration | [ ] |
|
||||||
|
| 20.6 | It should hide the Yodlee refresh button from non-admin users | Integration | [ ] |
|
||||||
|
|
||||||
### Role Access Summary
|
### Bank Account Search Behaviors
|
||||||
- **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
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 21.1 | It should provide a bank account typeahead for searching accounts belonging to a specific client | Integration | [ ] |
|
||||||
|
| 21.2 | It should show "Please select a client" message when no client is selected in the bank account typeahead | UI | [ ] |
|
||||||
|
|
||||||
### 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
|
## Test Data Requirements
|
||||||
|
|
||||||
### Clients
|
| Entity | Requirements |
|
||||||
- At least 2 clients with different codes, names, and addresses.
|
|--------|-------------|
|
||||||
- One client with a complete address (street1, street2, city, state, zip).
|
| **Clients** | Multiple clients with different codes, names, and addresses; one with complete address, one with no address; one with existing signature file, one without |
|
||||||
- One client with no address.
|
| **Vendors** | With 1099 data (legal entity name, TIN, TIN type, 1099 type, address); with and without addresses; who received $600+ in check payments in 2025; who received less than $600; shared across multiple clients |
|
||||||
- One client with an existing signature file URL.
|
| **Payments** | Check payments dated within 2025 for 1099 testing; payments of different types (check, ACH) — only checks count toward 1099; payments to vendors across multiple clients |
|
||||||
- One client with no signature file.
|
| **Invoices** | Non-voided invoices with expense account allocations for expense report testing; with different vendors and expense accounts; dated across multiple weeks for 8-week breakdown |
|
||||||
|
| **Bank Accounts** | Plaid-linked accounts with various statuses (success, error, unauthorized); Yodlee-linked accounts with various statuses |
|
||||||
|
| **Reports** | At least one generated report with name, creator, created date, and file 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) |
|
||||||
|
|
||||||
### Vendors
|
## Existing Tests to Preserve
|
||||||
- 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
|
- No existing company-specific behavior tests were identified at the time of writing.
|
||||||
- Check payments dated within 2025 (Jan 1 - Dec 31) for 1099 testing.
|
- Any future company behavior tests should be added under `test/clj/auto_ap/company/` or similar.
|
||||||
- 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
|
## Dependencies
|
||||||
|
|
||||||
### Backend
|
- Datomic (primary store for all entities and queries)
|
||||||
- Datomic for all entity storage and queries.
|
- Amazonica S3 (signature image storage and report file storage)
|
||||||
- Amazonica S3 for signature image storage and report file storage.
|
- Plaid API (bank account linking)
|
||||||
- Plaid API for bank account linking (token exchange, account fetch).
|
- Yodlee API (bank account linking)
|
||||||
- Yodlee API for bank account linking (Fastlink, token management).
|
- Intuit/QuickBooks API (reconciliation report data)
|
||||||
- Intuit/QuickBooks API for reconciliation report data.
|
- Solr (client name search)
|
||||||
- Solr for client name search (`company-search` endpoint).
|
- HTMX (server-rendered interactions)
|
||||||
|
- Alpine.js (signature canvas state, drag-and-drop, modal state, chart initialization)
|
||||||
### Frontend
|
- Chart.js (expense breakdown bar chart)
|
||||||
- HTMX for all server-rendered interactions.
|
- SignaturePad library (canvas signature drawing)
|
||||||
- Alpine.js for signature canvas state, drag-and-drop file upload, modal state, chart initialization.
|
- Plaid Link SDK (Plaid account linking)
|
||||||
- Chart.js for expense breakdown bar chart.
|
- Yodlee Fastlink SDK (Yodlee account linking)
|
||||||
- SignaturePad library for canvas signature drawing.
|
- Jdenticon (account identicons in Plaid grid)
|
||||||
- Plaid Link SDK for Plaid account linking.
|
|
||||||
- Yodlee Fastlink SDK for Yodlee account linking.
|
|
||||||
- Jdenticon for account identicons in Plaid grid.
|
|
||||||
|
|||||||
@@ -4,172 +4,247 @@
|
|||||||
|
|
||||||
The Dashboard is an admin-only, SSR-based overview page that displays financial summary cards across selected clients. It provides at-a-glance visibility into bank account balances, outstanding tasks, recent sales trends, expense breakdowns, and profit/loss summaries. Cards are loaded lazily via HTMX for progressive rendering.
|
The Dashboard is an admin-only, SSR-based overview page that displays financial summary cards across selected clients. It provides at-a-glance visibility into bank account balances, outstanding tasks, recent sales trends, expense breakdowns, and profit/loss summaries. Cards are loaded lazily via HTMX for progressive rendering.
|
||||||
|
|
||||||
## Routes
|
**Testing Philosophy**
|
||||||
|
- Prefer unit tests for pure business logic (calculations, formatting, client trimming)
|
||||||
|
- Use integration tests for database interactions and cross-system flows (Datomic queries, GraphQL calls)
|
||||||
|
- Use UI tests only for end-to-end happy paths and visual states
|
||||||
|
- Every behavior must be user-visible; no tests for implementation details
|
||||||
|
|
||||||
| Route | Handler | Purpose |
|
---
|
||||||
|-------|---------|---------|
|
|
||||||
| `GET /` | `::page` | Main dashboard page with stub cards |
|
|
||||||
| `GET /expense-card` | `::expense-card` | Expense pie chart (top 5 accounts, last month) |
|
|
||||||
| `GET /pnl-card` | `::pnl-card` | Profit & Loss bar chart (last month) |
|
|
||||||
| `GET /sales-card` | `::sales-card` | Gross sales bar chart (last 14 days) |
|
|
||||||
| `GET /bank-accounts-card` | `::bank-accounts-card` | Bank account balances and sync status |
|
|
||||||
| `GET /tasks-card` | `::tasks-card` | Unpaid invoices and uncategorized transactions |
|
|
||||||
|
|
||||||
## Behaviors
|
## Testing Patterns
|
||||||
|
|
||||||
### Unit Test Behaviors
|
### Pattern: Progressive Card Loading
|
||||||
|
Dashboard cards follow a progressive loading pattern:
|
||||||
|
1. SSR renders stub cards with loading spinners
|
||||||
|
2. Each stub triggers an independent HTMX request on load
|
||||||
|
3. Card endpoints return HTML fragments that replace the stub content
|
||||||
|
4. Cards load independently without blocking each other
|
||||||
|
|
||||||
- `extract-client-ids` returns intersection of user's allowed clients and requested clients
|
**Test implications:** Unit test the card query logic. Integration test each card endpoint returns valid HTML. UI test only needs to verify the page loads and cards appear.
|
||||||
- `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
|
### Pattern: Client Context Propagation
|
||||||
|
All dashboard operations depend on selected clients:
|
||||||
|
1. Client selection triggers a page-wide refresh via HTMX
|
||||||
|
2. `wrap-trim-clients` middleware limits to 20 clients before queries execute
|
||||||
|
3. All card endpoints receive the same trimmed client set
|
||||||
|
|
||||||
- Main page `GET /` returns 200 with base layout and 6 stub cards for admin users
|
**Test implications:** Integration test that changing clients updates all cards. Unit test the trimming logic.
|
||||||
- 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)
|
### Pattern: Admin Permission Gating
|
||||||
|
The dashboard is restricted to admin users:
|
||||||
|
1. `wrap-admin` middleware checks user role before any data access
|
||||||
|
2. Non-admin users are redirected before reaching handlers
|
||||||
|
3. Middleware is applied consistently to all card endpoints
|
||||||
|
|
||||||
#### Happy Path: Dashboard Loads Successfully
|
**Test implications:** Integration test each endpoint independently for permission enforcement.
|
||||||
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
|
## Dashboard Page
|
||||||
- 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
|
### Page Loading Behaviors
|
||||||
|
|
||||||
### No Clients Selected
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Dashboard page renders but all cards show empty state
|
|---|----------|---------------|--------|
|
||||||
- Bank Accounts card renders empty list
|
| 1.1 | It should render the main dashboard page with navigation, client selector, and "Dashboard" breadcrumb for admin users | UI | [ ] |
|
||||||
- Sales chart shows empty bar chart with no data points
|
| 1.2 | It should display six stub cards with loading spinners for progressive rendering | UI | [ ] |
|
||||||
- Expense pie chart shows empty pie chart
|
| 1.3 | It should trigger independent HTMX requests to load each card's content on page load | Integration | [ ] |
|
||||||
- P&L card shows zero income and expenses
|
| 1.4 | It should progressively replace stub cards with actual data as responses arrive | UI | [ ] |
|
||||||
- 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
|
## Bank Accounts Card
|
||||||
- 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
|
### Display Behaviors
|
||||||
- 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)
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Only users with `user/role = "admin"` can access dashboard routes
|
|---|----------|---------------|--------|
|
||||||
- Non-admin authenticated users receive 302 redirect to `/login`
|
| 2.1 | It should display each client's name, account name, ledger balance, and last sync time | UI | [ ] |
|
||||||
- Unauthenticated users receive 302 redirect to `/login` with `redirect-to` parameter
|
| 2.2 | It should exclude bank accounts with cash type from the display | Integration | [ ] |
|
||||||
- Admin role is verified by `wrap-admin` middleware before any data queries execute
|
| 2.3 | It should format ledger balances as currency ($X,XXX.XX) | Unit + UI | [ ] |
|
||||||
|
| 2.4 | It should display the last sync timestamp in standard time format when present | Unit + UI | [ ] |
|
||||||
|
| 2.5 | It should display Intuit balance and sync time for Intuit-linked accounts | UI | [ ] |
|
||||||
|
| 2.6 | It should display Yodlee available balance, sync time, and pending balance for Yodlee-linked accounts | UI | [ ] |
|
||||||
|
| 2.7 | It should display Plaid balance and sync time for Plaid-linked accounts | UI | [ ] |
|
||||||
|
| 2.8 | It should display $0.00 for missing or null balances | Unit + UI | [ ] |
|
||||||
|
|
||||||
### Bank Account Sync States
|
---
|
||||||
- Account with no external sync (Intuit/Yodlee/Plaid): Shows only ledger balance and sync time
|
|
||||||
- Account with Intuit sync: Shows Intuit balance and last synced timestamp
|
## Sales Card
|
||||||
- Account with Yodlee sync: Shows available balance, last synced timestamp, and pending balance
|
|
||||||
- Account with Plaid sync: Shows Plaid balance and last synced timestamp
|
### Display Behaviors
|
||||||
- Missing/null balances display as `$0.00`
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 3.1 | It should display a bar chart of gross sales for the last 14 days | UI | [ ] |
|
||||||
|
| 3.2 | It should render an empty bar chart when no sales orders exist in the date range | UI | [ ] |
|
||||||
|
|
||||||
|
### Data Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 3.3 | It should query and sum sales order totals by date for the selected clients | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Expense Card
|
||||||
|
|
||||||
|
### Display Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 4.1 | It should display a pie chart of the top 5 expense accounts for the last month | UI | [ ] |
|
||||||
|
| 4.2 | It should render an empty pie chart when no invoices with expense accounts exist in the date range | UI | [ ] |
|
||||||
|
|
||||||
|
### Data Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 4.3 | It should sum expense amounts by account name for the selected clients | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Profit & Loss Card
|
||||||
|
|
||||||
|
### Display Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 5.1 | It should display income and expenses aggregated by category (sales, COGS, payroll, controllable, fixed overhead, ownership controllable) | UI | [ ] |
|
||||||
|
| 5.2 | It should show $0.00 for both income and expenses when no data exists for the period | UI | [ ] |
|
||||||
|
|
||||||
|
### Data Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 5.3 | It should query P&L data via GraphQL for the selected clients and last month | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tasks Card
|
||||||
|
|
||||||
|
### Display Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 6.1 | It should display the count of unpaid invoices when the count is non-zero | UI | [ ] |
|
||||||
|
| 6.2 | It should display the count of uncategorized transactions requiring feedback when the count is non-zero | UI | [ ] |
|
||||||
|
| 6.3 | It should provide a "Pay now" link for unpaid invoices linking to the unpaid invoices page with year date range | UI | [ ] |
|
||||||
|
| 6.4 | It should provide a "Review now" link for uncategorized transactions linking to the requires-feedback page | UI | [ ] |
|
||||||
|
| 6.5 | It should hide task sections entirely when their respective counts are zero | Integration | [ ] |
|
||||||
|
|
||||||
|
### Data Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 6.6 | It should query Datomic for invoices with unpaid status for the selected clients | Integration | [ ] |
|
||||||
|
| 6.7 | It should query Datomic for transactions with requires-feedback approval status for the selected clients | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Expense Breakdown Card
|
||||||
|
|
||||||
|
### Display Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 7.1 | It should display a bar chart breaking down expenses by account | UI | [ ] |
|
||||||
|
| 7.2 | It should render an empty chart when no expense data exists | UI | [ ] |
|
||||||
|
| 7.3 | It should provide Vendor and Account typeahead filters | UI | [ ] |
|
||||||
|
|
||||||
|
### Data Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 7.4 | It should reload the chart with filtered data when filter selections change | Integration | [ ] |
|
||||||
|
| 7.5 | It should update the URL with filter query parameters via hx-push-url | Integration | [ ] |
|
||||||
|
| 7.6 | It should exclude voided invoices from the breakdown | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Filtering Behaviors
|
||||||
|
|
||||||
|
### Expense Breakdown Filters
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 8.1 | It should filter the expense breakdown chart by vendor selection | Integration | [ ] |
|
||||||
|
| 8.2 | It should filter the expense breakdown chart by expense account selection | Integration | [ ] |
|
||||||
|
| 8.3 | It should trigger an HTMX request to reload the chart when any filter changes | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Client Selection Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 9.1 | It should update the dashboard content when the user selects different clients from the dropdown | UI | [ ] |
|
||||||
|
| 9.2 | It should trigger a clientSelected event on the body when client selection changes | Integration | [ ] |
|
||||||
|
| 9.3 | It should swap the dashboard content area with fresh content for the newly selected clients | Integration | [ ] |
|
||||||
|
| 9.4 | It should re-fetch all card data with the new client context | Integration | [ ] |
|
||||||
|
| 9.5 | It should limit reports to the first 20 selected clients from the valid set | Unit + Integration | [ ] |
|
||||||
|
| 9.6 | It should display a yellow warning banner when more than 20 clients are selected | UI | [ ] |
|
||||||
|
| 9.7 | It should persist the warning banner across client selection changes until fewer than 21 clients are selected | UI | [ ] |
|
||||||
|
| 9.8 | It should trim the client set before executing any card data queries | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 10.1 | It should load each card independently via separate HTMX requests | Integration | [ ] |
|
||||||
|
| 10.2 | It should not prevent other cards from loading when one card endpoint fails | Integration | [ ] |
|
||||||
|
| 10.3 | It should display a loading spinner on stub cards until data loads or a timeout occurs | UI | [ ] |
|
||||||
|
| 10.4 | It should return appropriate HTTP status codes for card endpoint errors without breaking the page layout | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-Cutting Behaviors
|
||||||
|
|
||||||
|
### Permission Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 11.1 | It should allow only admin users to access the dashboard page and card endpoints | Integration | [ ] |
|
||||||
|
| 11.2 | It should redirect non-admin authenticated users to /login with a 302 status | Integration | [ ] |
|
||||||
|
| 11.3 | It should redirect unauthenticated users to /login with a redirect-to parameter | Integration | [ ] |
|
||||||
|
| 11.4 | It should verify admin role via middleware before executing any data queries | Integration | [ ] |
|
||||||
|
|
||||||
|
### Empty State Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 12.1 | It should render the dashboard page when no clients are selected, with all cards showing empty states | UI | [ ] |
|
||||||
|
| 12.2 | It should display an empty bank accounts list when no clients are selected | UI | [ ] |
|
||||||
|
| 12.3 | It should display an empty sales chart when no clients are selected | UI | [ ] |
|
||||||
|
| 12.4 | It should display an empty expense pie chart when no clients are selected | UI | [ ] |
|
||||||
|
| 12.5 | It should show $0.00 income and expenses in the P&L card when no clients are selected | UI | [ ] |
|
||||||
|
| 12.6 | It should hide all task sections when no clients are selected | UI | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Test Data Requirements
|
## Test Data Requirements
|
||||||
|
|
||||||
### Users
|
| Entity | Requirements |
|
||||||
- Admin user (`:user/role "admin"`) with access to multiple clients
|
|--------|-------------|
|
||||||
- Non-admin user (`:user/role "user"`) to test permission denial
|
| **Users** | Admin user with access to multiple clients; non-admin user for permission denial |
|
||||||
|
| **Clients** | Minimum 2, ideally 25+; mix with/without bank accounts |
|
||||||
|
| **Bank Accounts** | Various types (checking, savings, cash); some linked to Intuit, Yodlee, Plaid; with/without balances and sync timestamps |
|
||||||
|
| **Sales Orders** | Orders within and outside the 14-day window with totals |
|
||||||
|
| **Invoices** | With expense accounts and unpaid status; voided invoices to test exclusion |
|
||||||
|
| **Transactions** | With requires-feedback approval status |
|
||||||
|
| **Chart of Accounts** | Categories: sales, COGS, payroll, controllable, fixed overhead, ownership controllable |
|
||||||
|
|
||||||
### Clients (minimum 2, ideally 25+)
|
## Existing Tests to Preserve
|
||||||
- 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
|
- No existing dashboard-specific tests have been identified in the current test suite. Any tests covering dashboard routes or card handlers should be preserved during refactoring.
|
||||||
- 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
|
## Dependencies
|
||||||
|
|
||||||
### External Services
|
- Datomic (primary data store for all card queries)
|
||||||
- **Datomic**: All card data queried from Datomic database
|
- GraphQL/Lacinia (P&L data via get-profit-and-loss-raw)
|
||||||
- **GraphQL endpoint**: P&L card calls `get-profit-and-loss-raw` (Lacinia GraphQL)
|
- HTMX (progressive card loading via hx-get/hx-trigger/hx-swap)
|
||||||
- **Intuit/Yodlee/Plaid**: Bank account sync data for external balances (data stored in Datomic)
|
- Chart.js (canvas-based charts)
|
||||||
|
- Alpine.js (chart data binding)
|
||||||
### 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
|
|
||||||
|
|||||||
@@ -4,364 +4,516 @@
|
|||||||
|
|
||||||
The Ledger module is the core accounting interface of Integreat. It provides server-side rendered (HTMX) pages for viewing journal entries, creating manual journal entries, and generating financial reports (Profit & Loss, Balance Sheet, Cash Flows). All ledger pages are permission-gated and client-scoped.
|
The Ledger module is the core accounting interface of Integreat. It provides server-side rendered (HTMX) pages for viewing journal entries, creating manual journal entries, and generating financial reports (Profit & Loss, Balance Sheet, Cash Flows). All ledger pages are permission-gated and client-scoped.
|
||||||
|
|
||||||
## Routes & Pages
|
**Testing Philosophy**
|
||||||
|
- Prefer unit tests for pure business logic (calculations, validations, transformations)
|
||||||
|
- Use integration tests for database interactions and cross-system flows
|
||||||
|
- Use UI tests only for end-to-end happy paths that touch multiple pages
|
||||||
|
- Every behavior must be user-visible; no tests for implementation details
|
||||||
|
|
||||||
| Route | Method | Handler | Description |
|
---
|
||||||
|-------|--------|---------|-------------|
|
|
||||||
| `/ledger` | GET | `::all-page` | Main ledger entries list (internal) |
|
|
||||||
| `/ledger/external-new` | GET | `::external-page` | External ledger entries list |
|
|
||||||
| `/ledger/new` | GET | `::new` | Modal form for new journal entry |
|
|
||||||
| `/ledger/new` | POST | `::new-submit` | Submit new journal entry |
|
|
||||||
| `/ledger/new/location-select` | GET | `::location-select` | Location dropdown for line item |
|
|
||||||
| `/ledger/new/account-typeahead` | GET | `::account-typeahead` | Account search for line item |
|
|
||||||
| `/ledger/new/line-item` | GET | `::new-line-item` | Add new line item row |
|
|
||||||
| `/ledger/external-import-new` | GET | `::external-import-page` | External TSV import page |
|
|
||||||
| `/ledger/external-import-new/parse` | POST | `::external-import-parse` | Parse clipboard TSV data |
|
|
||||||
| `/ledger/external-import-new/import` | POST | `::external-import-import` | Import validated entries |
|
|
||||||
| `/ledger/investigate` | GET | `::investigate` | Investigation modal for report drill-down |
|
|
||||||
| `/ledger/investigate/results` | GET | `::investigate-results` | Investigation results table |
|
|
||||||
| `/ledger/table` | GET | `::table` | HTMX table fragment for ledger entries |
|
|
||||||
| `/ledger/csv` | GET | `::csv` | CSV export of ledger entries |
|
|
||||||
| `/ledger/bank-account-filter` | GET | `::bank-account-filter` | Bank account filter widget |
|
|
||||||
| `/ledger/reports/profit-and-loss` | GET | `::profit-and-loss` | P&L report page |
|
|
||||||
| `/ledger/reports/profit-and-loss/run` | PUT | `::run-profit-and-loss` | Run P&L report |
|
|
||||||
| `/ledger/reports/profit-and-loss/export` | POST | `::export-profit-and-loss` | Export P&L to PDF |
|
|
||||||
| `/ledger/reports/balance-sheet` | GET | `::balance-sheet` | Balance sheet page |
|
|
||||||
| `/ledger/reports/balance-sheet/run` | GET | `::run-balance-sheet` | Run balance sheet |
|
|
||||||
| `/ledger/reports/balance-sheet/export` | POST | `::export-balance-sheet` | Export balance sheet to PDF |
|
|
||||||
| `/ledger/reports/cash-flows` | GET | `::cash-flows` | Cash flows page |
|
|
||||||
| `/ledger/reports/cash-flows/run` | PUT | `::run-cash-flows` | Run cash flows report |
|
|
||||||
| `/ledger/reports/cash-flows/export` | POST | `::export-cash-flows` | Export cash flows to PDF |
|
|
||||||
|
|
||||||
## Behaviors by Page
|
## Testing Patterns
|
||||||
|
|
||||||
### Ledger Entries List
|
### Pattern: Grid Page Behaviors
|
||||||
|
Most list pages in Integreat follow the same pattern:
|
||||||
|
1. Fetch IDs via Datomic query with filters
|
||||||
|
2. Hydrate results via `pull-many`
|
||||||
|
3. Render table with sortable columns
|
||||||
|
4. Support selection (individual / all / all-filtered)
|
||||||
|
5. Action buttons appear conditionally based on permissions and selection state
|
||||||
|
|
||||||
**Page load**
|
**Test implications:** Validate the query generation, not the rendering. UI tests only need to verify one filter and one sort work end-to-end.
|
||||||
- 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**
|
### Pattern: Modal Form Behaviors
|
||||||
- Client (hidden if only 1 client with 1 location)
|
Modal forms are HTMX-driven dialogs:
|
||||||
- Vendor (falls back to `alternate-description` if no vendor)
|
1. Opened via GET request that renders a form fragment
|
||||||
- Source (hidden on internal ledger)
|
2. Form submissions are POST/PUT with validation
|
||||||
- External ID (hidden on internal ledger, truncated to max-width)
|
3. On success, the modal closes and the table updates in place
|
||||||
- Date
|
4. On validation failure, the modal shows error messages
|
||||||
- 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**
|
**Test implications:** Unit test validation logic. Integration test the full modal flow once. UI test only the happy path.
|
||||||
- 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**
|
### Pattern: Report Behaviors
|
||||||
- Available sorts: Client, Vendor, Source, External ID, Date, Amount, Account
|
Financial reports follow a consistent pattern:
|
||||||
- Default sort: Date ascending
|
1. Form with client multi-select, date/period selectors, and toggles
|
||||||
- Sorting by Vendor or Source groups rows with break headers
|
2. Run button triggers HTMX request to generate report
|
||||||
|
3. Report data is computed from account snapshots and running balances
|
||||||
|
4. Export button generates PDF and returns a download modal
|
||||||
|
|
||||||
**Filtering (left sidebar)**
|
**Test implications:** Unit test calculation logic. Integration test report generation with various filter combinations. UI test only one report end-to-end.
|
||||||
- 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**
|
### Pattern: Permission Gates
|
||||||
- Default 25 entries per page
|
Every mutating operation checks:
|
||||||
- Configurable per-page
|
1. `assert-can-see-client` — user has access to the client
|
||||||
- Pagination controls at bottom
|
2. `assert-not-locked` — entry date >= client locked-until
|
||||||
|
3. `can?` — user has the specific permission for the activity
|
||||||
|
|
||||||
**CSV Export**
|
**Test implications:** Integration test each gate independently. UI tests only verify the happy path with a permitted user.
|
||||||
- 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**
|
## Ledger Entries List
|
||||||
- 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**
|
### Display 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**
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Client is required
|
|---|----------|---------------|--------|
|
||||||
- Date is required and must be valid
|
| 1.1 | It should display a paginated, sortable data grid of journal entries | UI | [ ] |
|
||||||
- Vendor is required
|
| 1.2 | It should show the Client column only when multiple clients OR multiple locations are selected | Integration | [ ] |
|
||||||
- Amount must be ≥ $0.01
|
| 1.3 | It should display the Vendor column, falling back to `alternate-description` when no vendor is present | Integration | [ ] |
|
||||||
- Each line item must have an allowed account
|
| 1.4 | It should hide the Source column on the internal ledger page | UI | [ ] |
|
||||||
- Each line item must have a location belonging to the account
|
| 1.5 | It should hide the External ID column on the internal ledger page | UI | [ ] |
|
||||||
- Debits must sum to total amount
|
| 1.6 | It should truncate the External ID column to a max-width when displayed | UI | [ ] |
|
||||||
- Credits must sum to total amount
|
| 1.7 | It should display the Date column with formatted dates | UI | [ ] |
|
||||||
- Debits and credits must each equal the journal entry amount
|
| 1.8 | It should display the Amount column formatted as currency | UI | [ ] |
|
||||||
|
| 1.9 | It should display Debit lines with account, location, and amount per line item | UI | [ ] |
|
||||||
|
| 1.10 | It should display Credit lines with account, location, and amount per line item | UI | [ ] |
|
||||||
|
| 1.11 | It should display a Links dropdown with links to original invoice, source file, transaction, or memo | UI | [ ] |
|
||||||
|
| 1.12 | It should show the page title reflecting the status filter, e.g. "Unpaid Register" or "Paid Register" | UI | [ ] |
|
||||||
|
| 1.13 | It should show an "Add journal entry" button on the internal ledger page | UI | [ ] |
|
||||||
|
| 1.14 | It should hide the "Add journal entry" button on the external ledger page | UI | [ ] |
|
||||||
|
| 1.15 | It should show a client selection sidebar when the user has access to multiple clients | UI | [ ] |
|
||||||
|
|
||||||
**On save**
|
### Filtering Behaviors
|
||||||
- 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
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 2.1 | It should filter entries by vendor typeahead selection | Integration | [ ] |
|
||||||
|
| 2.2 | It should filter entries by account typeahead selection | Integration | [ ] |
|
||||||
|
| 2.3 | It should filter entries by bank account via radio filter | Integration | [ ] |
|
||||||
|
| 2.4 | It should refresh the bank account filter when the client selection changes | Integration | [ ] |
|
||||||
|
| 2.5 | It should filter entries by date range | Integration | [ ] |
|
||||||
|
| 2.6 | It should filter entries by invoice number text search | Integration | [ ] |
|
||||||
|
| 2.7 | It should filter entries by account code range (gte/lte inputs) | Integration | [ ] |
|
||||||
|
| 2.8 | It should filter entries by amount range (gte/lte inputs) | Integration | [ ] |
|
||||||
|
| 2.9 | It should filter to unbalanced entries when "Show unbalanced" is checked | Integration | [ ] |
|
||||||
|
| 2.10 | It should support exact-match navigation to a specific entry by ID, bypassing other filters | Integration | [ ] |
|
||||||
|
| 2.11 | It should clear the exact match ID pill when clicked | UI | [ ] |
|
||||||
|
| 2.12 | Given multiple filters are applied, when the user changes one filter, then the table should refresh with the combined filter set | Integration | [ ] |
|
||||||
|
|
||||||
**Clipboard paste**
|
### Sorting Behaviors
|
||||||
- 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**
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Validates all rows have required fields
|
|---|----------|---------------|--------|
|
||||||
- Dates must be parseable
|
| 3.1 | It should sort by Client ascending/descending | Integration | [ ] |
|
||||||
- Account codes must be numeric or bank account strings
|
| 3.2 | It should sort by Vendor ascending/descending | Integration | [ ] |
|
||||||
- Locations must be 1-2 characters
|
| 3.3 | It should sort by Source ascending/descending | Integration | [ ] |
|
||||||
- Debits/Credits must be valid money amounts
|
| 3.4 | It should sort by External ID ascending/descending | Integration | [ ] |
|
||||||
|
| 3.5 | It should sort by Date ascending/descending | Integration | [ ] |
|
||||||
|
| 3.6 | It should sort by Amount ascending/descending | Integration | [ ] |
|
||||||
|
| 3.7 | It should sort by Account ascending/descending | Integration | [ ] |
|
||||||
|
| 3.8 | It should default to Date ascending | Integration | [ ] |
|
||||||
|
| 3.9 | Given sorting by Vendor, then rows should be grouped with break headers | Integration | [ ] |
|
||||||
|
| 3.10 | Given sorting by Source, then rows should be grouped with break headers | Integration | [ ] |
|
||||||
|
| 3.11 | Given the user clicks a column header twice, then the sort direction should toggle | Integration | [ ] |
|
||||||
|
|
||||||
**Import validation**
|
### Pagination Behaviors
|
||||||
- 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**
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Successful entries are imported
|
|---|----------|---------------|--------|
|
||||||
- Entries with warnings are ignored (removed if previously existed)
|
| 4.1 | It should display 25 entries per page by default | Integration | [ ] |
|
||||||
- Entries with errors block import and show error counts
|
| 4.2 | It should allow changing the per-page count | Integration | [ ] |
|
||||||
- Retracts existing entries by external ID before importing
|
| 4.3 | It should show pagination controls at the bottom of the table | UI | [ ] |
|
||||||
- Indexes imported entries in Solr asynchronously
|
|
||||||
|
|
||||||
### P&L Report
|
### Row Action Behaviors
|
||||||
|
|
||||||
**Form**
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Customer multi-select typeahead (max 20, defaults to first 5 if "all")
|
|---|----------|---------------|--------|
|
||||||
- Periods dropdown (default: year-to-date)
|
| 5.1 | It should show a void button for unpaid invoices when the user has `:delete :invoice` permission | UI | [ ] |
|
||||||
- "Column per location" toggle
|
| 5.2 | It should show an edit button for unpaid and paid invoices when the user has `:edit :invoice` permission | UI | [ ] |
|
||||||
- "Include deltas" toggle
|
| 5.3 | It should show an unvoid button for voided invoices when the user has `:edit :invoice` permission | UI | [ ] |
|
||||||
- Run button (HTMX PUT)
|
| 5.4 | It should show a trash icon with confirmation for delete operations | UI | [ ] |
|
||||||
- Export PDF button
|
|
||||||
|
|
||||||
**Report generation**
|
### CSV Export Behaviors
|
||||||
- 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**
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Summary table: Sales, COGS, Payroll, Gross Profits, Overhead, Net Income
|
|---|----------|---------------|--------|
|
||||||
- Detail table: Account-level breakdown within each category
|
| 6.1 | It should export all matching entries with line-item-level rows | Integration | [ ] |
|
||||||
- Percent of sales calculated for each row
|
| 6.2 | It should include columns: ID, Client, Vendor, Source, External ID, Date, Amount, Account, Debit, Credit | Integration | [ ] |
|
||||||
- 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**
|
## New Journal Entry
|
||||||
- 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
|
### Modal Form Behaviors
|
||||||
|
|
||||||
**Form**
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Customer multi-select typeahead (max 20, defaults to first 5 if "all")
|
|---|----------|---------------|--------|
|
||||||
- Date dropdown (single date, default today)
|
| 7.1 | It should open as a modal dialog 750px wide | UI | [ ] |
|
||||||
- "Include deltas" toggle
|
| 7.2 | It should show a client typeahead pre-filled if a client is already selected on the parent page | UI | [ ] |
|
||||||
- Run button (HTMX GET)
|
| 7.3 | It should show a date input defaulting to today in MM/DD/YYYY format | UI | [ ] |
|
||||||
- Export PDF button
|
| 7.4 | It should show a vendor typeahead disabled when editing an existing entry | UI | [ ] |
|
||||||
|
| 7.5 | It should show a total amount input requiring a value of at least $0.01 | Unit + Integration | [ ] |
|
||||||
|
| 7.6 | It should show an optional memo text input | UI | [ ] |
|
||||||
|
| 7.7 | It should display a line items grid with Account, Location, Debit, and Credit columns | UI | [ ] |
|
||||||
|
|
||||||
**Report generation**
|
### Line Item Behaviors
|
||||||
- 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**
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Assets section with account detail and subtotal
|
|---|----------|---------------|--------|
|
||||||
- Liabilities section with account detail and subtotal
|
| 8.1 | It should allow account typeahead search scoped to the selected client | Integration | [ ] |
|
||||||
- Owner's Equity section with account detail and subtotal
|
| 8.2 | It should update the location dropdown based on the selected account's required location | Integration | [ ] |
|
||||||
- Retained Earnings line
|
| 8.3 | It should lock the location dropdown to a fixed location when the account requires it | Integration | [ ] |
|
||||||
- Delta columns between periods if enabled and multiple dates selected
|
| 8.4 | It should show all client locations when the account has no location restriction | Integration | [ ] |
|
||||||
|
| 8.5 | It should add new line item rows via HTMX request | UI | [ ] |
|
||||||
|
| 8.6 | It should allow removing line item rows with an X button | UI | [ ] |
|
||||||
|
|
||||||
**Warnings**
|
### Validation Behaviors
|
||||||
- Warns if >20 clients selected
|
|
||||||
- Warns about unresolved ledger entries
|
|
||||||
|
|
||||||
**PDF Export**
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Generates PDF, uploads to S3 at `reports/balance-sheet/<uuid>/<name>.pdf`
|
|---|----------|---------------|--------|
|
||||||
- Persists report record in Datomic
|
| 9.1 | It should require a client | Unit + Integration | [ ] |
|
||||||
|
| 9.2 | It should require a valid date | Unit + Integration | [ ] |
|
||||||
|
| 9.3 | It should require a vendor | Unit + Integration | [ ] |
|
||||||
|
| 9.4 | It should require an amount of at least $0.01 | Unit + Integration | [ ] |
|
||||||
|
| 9.5 | It should require each line item to have an allowed account | Unit + Integration | [ ] |
|
||||||
|
| 9.6 | It should require each line item to have a location belonging to the account | Unit + Integration | [ ] |
|
||||||
|
| 9.7 | It should validate that debits sum to the total amount | Unit + Integration | [ ] |
|
||||||
|
| 9.8 | It should validate that credits sum to the total amount | Unit + Integration | [ ] |
|
||||||
|
| 9.9 | It should validate that debits and credits each equal the journal entry amount | Unit + Integration | [ ] |
|
||||||
|
| 9.10 | It should block saving when the entry date is on or before the client's `locked-until` date | Integration | [ ] |
|
||||||
|
|
||||||
### Cash Flows
|
### Save Behaviors
|
||||||
|
|
||||||
**Form**
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Customer multi-select typeahead (max 20, defaults to first 5 if "all")
|
|---|----------|---------------|--------|
|
||||||
- Periods dropdown (default: year-to-date)
|
| 10.1 | It should generate an external ID in the format `manual-<uuid>` | Unit | [ ] |
|
||||||
- Run button (HTMX PUT)
|
| 10.2 | It should update the client's `ledger-last-change` timestamp | Integration | [ ] |
|
||||||
- Export PDF button
|
| 10.3 | Given a new entry is saved successfully, then it should prepend the new row to the table and close the modal | UI | [ ] |
|
||||||
|
| 10.4 | Given an existing entry is saved successfully, then it should replace the existing row in the table and close the modal | UI | [ ] |
|
||||||
|
|
||||||
**Report generation**
|
---
|
||||||
- Queries account snapshot as of period end + 1 day
|
|
||||||
- Groups accounts into: Operating Activities, Investment Activities, Financing Activities, Cash
|
|
||||||
- Calculates cash flow effect: add or subtract based on account code ranges
|
|
||||||
|
|
||||||
**Report output**
|
## External Import
|
||||||
- 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**
|
### Clipboard Paste Behaviors
|
||||||
- Warns if >20 clients selected
|
|
||||||
- Warns about unresolved ledger entries
|
|
||||||
|
|
||||||
**PDF Export**
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Generates PDF, uploads to S3 at `reports/cash-flows/<uuid>/<name>.pdf`
|
|---|----------|---------------|--------|
|
||||||
- Persists report record in Datomic
|
| 11.1 | It should allow clicking a "Load from clipboard" button | UI | [ ] |
|
||||||
|
| 11.2 | It should read TSV data from the browser clipboard | UI | [ ] |
|
||||||
|
| 11.3 | It should parse tab-separated values with columns: Id, Client, Source, Vendor, Date, Account Code, Location, Debit, Credit | Integration | [ ] |
|
||||||
|
|
||||||
### Investigation
|
### Parse Validation Behaviors
|
||||||
|
|
||||||
**Modal behavior**
|
| # | Behavior | Test Strategy | Status |
|
||||||
- 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)
|
| 12.1 | It should validate that all rows have required fields | Integration | [ ] |
|
||||||
- Displays raw table without checkboxes
|
| 12.2 | It should validate that dates are parseable | Unit + Integration | [ ] |
|
||||||
- Max height 600px with scrollable content
|
| 12.3 | It should validate that account codes are numeric or bank account strings | Unit + Integration | [ ] |
|
||||||
|
| 12.4 | It should validate that locations are 1-2 characters | Unit + Integration | [ ] |
|
||||||
|
| 12.5 | It should validate that debits and credits are valid money amounts | Unit + Integration | [ ] |
|
||||||
|
|
||||||
**Table behavior**
|
### Import Validation Behaviors
|
||||||
- Uses same query schema as main ledger list
|
|
||||||
- Supports sorting and pagination
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Does not push URL state
|
|---|----------|---------------|--------|
|
||||||
|
| 13.1 | It should validate that the client code exists | Integration | [ ] |
|
||||||
|
| 13.2 | It should validate that the vendor exists, creating a hidden vendor if missing | Integration | [ ] |
|
||||||
|
| 13.3 | It should block entries for dates when the client is locked | Integration | [ ] |
|
||||||
|
| 13.4 | It should validate that debits and credits balance per entry | Unit + Integration | [ ] |
|
||||||
|
| 13.5 | It should warn when an entry totals $0.00 | Unit + Integration | [ ] |
|
||||||
|
| 13.6 | It should validate that the location belongs to the client | Integration | [ ] |
|
||||||
|
| 13.7 | It should validate that the account code exists | Integration | [ ] |
|
||||||
|
| 13.8 | It should validate that bank account codes belong to the client | Integration | [ ] |
|
||||||
|
| 13.9 | It should validate that account location requirements are satisfied | Integration | [ ] |
|
||||||
|
|
||||||
|
### Import Result Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 14.1 | It should import successful entries | Integration | [ ] |
|
||||||
|
| 14.2 | It should ignore entries with warnings, removing them if they previously existed | Integration | [ ] |
|
||||||
|
| 14.3 | It should block import and show error counts when entries have errors | Integration | [ ] |
|
||||||
|
| 14.4 | It should retract existing entries by external ID before importing | Integration | [ ] |
|
||||||
|
| 14.5 | It should index imported entries in Solr asynchronously | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P&L Report
|
||||||
|
|
||||||
|
### Form Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 15.1 | It should show a customer multi-select typeahead with a max of 20 selections | UI | [ ] |
|
||||||
|
| 15.2 | It should default to the first 5 customers when "all" is selected | Integration | [ ] |
|
||||||
|
| 15.3 | It should show a periods dropdown defaulting to year-to-date | UI | [ ] |
|
||||||
|
| 15.4 | It should show a "Column per location" toggle | UI | [ ] |
|
||||||
|
| 15.5 | It should show an "Include deltas" toggle | UI | [ ] |
|
||||||
|
| 15.6 | It should trigger report generation via HTMX PUT on the Run button | UI | [ ] |
|
||||||
|
| 15.7 | It should show an Export PDF button | UI | [ ] |
|
||||||
|
|
||||||
|
### Report Generation Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 16.1 | It should compute running balances before generating the report | Integration | [ ] |
|
||||||
|
| 16.2 | It should query detailed account snapshots for each client and period end date | Integration | [ ] |
|
||||||
|
| 16.3 | It should calculate amounts as debits minus credits for assets, dividends, and expenses | Unit | [ ] |
|
||||||
|
| 16.4 | It should calculate amounts as credits minus debits for liabilities, equity, and revenue | Unit | [ ] |
|
||||||
|
| 16.5 | It should group data by client, location, and period | Integration | [ ] |
|
||||||
|
|
||||||
|
### Report Output Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 17.1 | It should display a summary table with Sales, COGS, Payroll, Gross Profits, Overhead, and Net Income | UI | [ ] |
|
||||||
|
| 17.2 | It should display a detail table with account-level breakdown within each category | UI | [ ] |
|
||||||
|
| 17.3 | It should calculate percent of sales for each row | Unit | [ ] |
|
||||||
|
| 17.4 | It should show deltas between periods when enabled | UI | [ ] |
|
||||||
|
| 17.5 | It should show each location as separate columns when column-per-location mode is enabled | UI | [ ] |
|
||||||
|
|
||||||
|
### Warning Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 18.1 | It should warn when more than 20 clients are selected and truncate to 20 | Integration | [ ] |
|
||||||
|
| 18.2 | It should warn about unresolved ledger entries with missing numeric codes | Integration | [ ] |
|
||||||
|
| 18.3 | It should show sample links to admin history for invalid entries when the user has `:view :history` permission | Integration | [ ] |
|
||||||
|
|
||||||
|
### PDF Export Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 19.1 | It should generate a PDF with Calibri Light font at 6pt | Integration | [ ] |
|
||||||
|
| 19.2 | It should upload the PDF to S3 at `reports/profit-and-loss/<uuid>/<name>.pdf` | Integration | [ ] |
|
||||||
|
| 19.3 | It should persist a report record in Datomic | Integration | [ ] |
|
||||||
|
| 19.4 | It should return a modal with a download link | UI | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Balance Sheet
|
||||||
|
|
||||||
|
### Form Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 20.1 | It should show a customer multi-select typeahead with a max of 20 selections | UI | [ ] |
|
||||||
|
| 20.2 | It should default to the first 5 customers when "all" is selected | Integration | [ ] |
|
||||||
|
| 20.3 | It should show a date dropdown defaulting to today | UI | [ ] |
|
||||||
|
| 20.4 | It should show an "Include deltas" toggle | UI | [ ] |
|
||||||
|
| 20.5 | It should trigger report generation via HTMX GET on the Run button | UI | [ ] |
|
||||||
|
| 20.6 | It should show an Export PDF button | UI | [ ] |
|
||||||
|
|
||||||
|
### Report Generation Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 21.1 | It should compute running balances before generating the report | Integration | [ ] |
|
||||||
|
| 21.2 | It should query account snapshots as of each selected date | Integration | [ ] |
|
||||||
|
| 21.3 | It should group accounts into Assets, Liabilities, and Owner's Equity | Integration | [ ] |
|
||||||
|
| 21.4 | It should include Retained Earnings as net income across all P&L categories | Unit | [ ] |
|
||||||
|
|
||||||
|
### Report Output Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 22.1 | It should display an Assets section with account detail and subtotal | UI | [ ] |
|
||||||
|
| 22.2 | It should display a Liabilities section with account detail and subtotal | UI | [ ] |
|
||||||
|
| 22.3 | It should display an Owner's Equity section with account detail and subtotal | UI | [ ] |
|
||||||
|
| 22.4 | It should display a Retained Earnings line | UI | [ ] |
|
||||||
|
| 22.5 | It should show delta columns between periods when enabled and multiple dates are selected | UI | [ ] |
|
||||||
|
|
||||||
|
### Warning Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 23.1 | It should warn when more than 20 clients are selected | Integration | [ ] |
|
||||||
|
| 23.2 | It should warn about unresolved ledger entries | Integration | [ ] |
|
||||||
|
|
||||||
|
### PDF Export Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 24.1 | It should generate a PDF and upload to S3 at `reports/balance-sheet/<uuid>/<name>.pdf` | Integration | [ ] |
|
||||||
|
| 24.2 | It should persist a report record in Datomic | Integration | [ ] |
|
||||||
|
| 24.3 | It should return a modal with a download link | UI | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cash Flows
|
||||||
|
|
||||||
|
### Form Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 25.1 | It should show a customer multi-select typeahead with a max of 20 selections | UI | [ ] |
|
||||||
|
| 25.2 | It should default to the first 5 customers when "all" is selected | Integration | [ ] |
|
||||||
|
| 25.3 | It should show a periods dropdown defaulting to year-to-date | UI | [ ] |
|
||||||
|
| 25.4 | It should trigger report generation via HTMX PUT on the Run button | UI | [ ] |
|
||||||
|
| 25.5 | It should show an Export PDF button | UI | [ ] |
|
||||||
|
|
||||||
|
### Report Generation Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 26.1 | It should query account snapshots as of period end plus one day | Integration | [ ] |
|
||||||
|
| 26.2 | It should group accounts into Operating Activities, Investment Activities, Financing Activities, and Cash | Integration | [ ] |
|
||||||
|
| 26.3 | It should calculate cash flow effect by adding or subtracting based on account code ranges | Unit | [ ] |
|
||||||
|
|
||||||
|
### Report Output Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 27.1 | It should display Net Income as the starting point | UI | [ ] |
|
||||||
|
| 27.2 | It should display Operating Activities detail with increases, decreases, and cash impact | UI | [ ] |
|
||||||
|
| 27.3 | It should display Investment Activities detail | UI | [ ] |
|
||||||
|
| 27.4 | It should display Financing Activities detail | UI | [ ] |
|
||||||
|
| 27.5 | It should display Change in Cash and Cash Equivalents total | UI | [ ] |
|
||||||
|
| 27.6 | It should display Bank Accounts / Cash detail | UI | [ ] |
|
||||||
|
|
||||||
|
### Warning Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 28.1 | It should warn when more than 20 clients are selected | Integration | [ ] |
|
||||||
|
| 28.2 | It should warn about unresolved ledger entries | Integration | [ ] |
|
||||||
|
|
||||||
|
### PDF Export Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 29.1 | It should generate a PDF and upload to S3 at `reports/cash-flows/<uuid>/<name>.pdf` | Integration | [ ] |
|
||||||
|
| 29.2 | It should persist a report record in Datomic | Integration | [ ] |
|
||||||
|
| 29.3 | It should return a modal with a download link | UI | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Investigation
|
||||||
|
|
||||||
|
### Modal Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 30.1 | It should open as a modal dialog from report table cell clicks | UI | [ ] |
|
||||||
|
| 30.2 | It should filter ledger entries by the clicked cell's filters: account code range, client, location, and date range | Integration | [ ] |
|
||||||
|
| 30.3 | It should display a raw table without checkboxes | UI | [ ] |
|
||||||
|
| 30.4 | It should constrain the modal to a max height of 600px with scrollable content | UI | [ ] |
|
||||||
|
|
||||||
|
### Table Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 31.1 | It should use the same query schema as the main ledger list | Integration | [ ] |
|
||||||
|
| 31.2 | It should support sorting and pagination | Integration | [ ] |
|
||||||
|
| 31.3 | It should not push URL state on filter or sort changes | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Cross-Cutting Behaviors
|
## Cross-Cutting Behaviors
|
||||||
|
|
||||||
### Report generation
|
### Report Generation Behaviors
|
||||||
- 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
|
| # | Behavior | Test Strategy | Status |
|
||||||
- All three reports support PDF export via `clj-pdf`
|
|---|----------|---------------|--------|
|
||||||
- PDFs use Calibri Light 6pt font on letter size
|
| 32.1 | It should call `upsert-running-balance` before querying to ensure cached balances are current | Integration | [ ] |
|
||||||
- Uploaded to S3 data bucket with UUID-based key
|
| 32.2 | It should use `detailed-account-snapshot` Datomic query for raw report data | Integration | [ ] |
|
||||||
- Report metadata persisted to Datomic (`:report/name`, `:report/client`, `:report/key`, `:report/url`, `:report/creator`, `:report/created`)
|
| 32.3 | It should build account lookups per-client via `build-account-lookup` | Integration | [ ] |
|
||||||
- Export returns modal with S3 download link
|
| 32.4 | It should skip entries without numeric codes and warn about unresolved entries | Integration | [ ] |
|
||||||
|
|
||||||
### Filtering and sorting
|
### Export Behaviors
|
||||||
- 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
|
| # | Behavior | Test Strategy | Status |
|
||||||
- All ledger pages require authenticated user
|
|---|----------|---------------|--------|
|
||||||
- Main ledger: `:read :ledger`
|
| 33.1 | It should support PDF export via `clj-pdf` for all three reports | Integration | [ ] |
|
||||||
- New/Edit journal entry: `:edit :ledger`
|
| 33.2 | It should use Calibri Light 6pt font on letter size for all PDF exports | Integration | [ ] |
|
||||||
- External import: `:import :ledger` + admin assertion
|
| 33.3 | It should upload PDFs to the S3 data bucket with a UUID-based key | Integration | [ ] |
|
||||||
- P&L report: `:read :profit-and-loss`
|
| 33.4 | It should persist report metadata to Datomic with name, client, key, URL, creator, and created timestamp | Integration | [ ] |
|
||||||
- Balance sheet: `:read :balance-sheet`
|
| 33.5 | It should return a modal with an S3 download link after export | UI | [ ] |
|
||||||
- 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
|
### Filtering and Sorting Behaviors
|
||||||
|
|
||||||
**Empty states**
|
| # | Behavior | Test Strategy | Status |
|
||||||
- No entries: empty table with pagination showing 0 results
|
|---|----------|---------------|--------|
|
||||||
- No matching filters: empty table, filter pills remain
|
| 34.1 | It should apply ledger list filters via HTMX on change with a 500ms debounce | Integration | [ ] |
|
||||||
- Report with no data: empty report form, no table rendered
|
| 34.2 | It should apply hot filters via HTMX on keyup with a 1000ms debounce | Integration | [ ] |
|
||||||
|
| 34.3 | It should refresh the bank account filter when client selection changes | Integration | [ ] |
|
||||||
|
| 34.4 | It should support multiple sort keys with ascending and descending direction | Integration | [ ] |
|
||||||
|
| 34.5 | It should default to date ascending sort | Integration | [ ] |
|
||||||
|
| 34.6 | It should bypass all other filters when an exact match ID filter is active | Integration | [ ] |
|
||||||
|
|
||||||
**Data locking**
|
### Permission Behaviors
|
||||||
- Journal entries cannot be created for dates on or before client's `locked-until` date
|
|
||||||
- External import rejects entries for locked dates
|
|
||||||
|
|
||||||
**Unbalanced entries**
|
| # | Behavior | Test Strategy | Status |
|
||||||
- "Show unbalanced" filter computes debit/credit sums per entry and filters to mismatches
|
|---|----------|---------------|--------|
|
||||||
- Unbalanced entries are still displayed in normal view
|
| 35.1 | It should require an authenticated user for all ledger pages | Integration | [ ] |
|
||||||
|
| 35.2 | It should require `:read :ledger` permission for the main ledger page | Integration | [ ] |
|
||||||
|
| 35.3 | It should require `:edit :ledger` permission for new and edit journal entry | Integration | [ ] |
|
||||||
|
| 35.4 | It should require `:import :ledger` permission plus admin assertion for external import | Integration | [ ] |
|
||||||
|
| 35.5 | It should require `:read :profit-and-loss` permission for the P&L report | Integration | [ ] |
|
||||||
|
| 35.6 | It should require `:read :balance-sheet` permission for the balance sheet | Integration | [ ] |
|
||||||
|
| 35.7 | It should require `:read :cash-flows` permission for the cash flows report | Integration | [ ] |
|
||||||
|
| 35.8 | It should restrict users to clients they have permission for via `assert-can-see-client` | Integration | [ ] |
|
||||||
|
| 35.9 | It should require `:delete :invoice` permission for invoice void actions | Integration | [ ] |
|
||||||
|
| 35.10 | It should require `:edit :invoice` permission for invoice edit and unvoid actions | Integration | [ ] |
|
||||||
|
|
||||||
**Account location mismatches**
|
### Empty State Behaviors
|
||||||
- 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**
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Capped at 20 clients for performance
|
|---|----------|---------------|--------|
|
||||||
- "All" clients defaults to first 5 for reports
|
| 36.1 | It should show an empty table with pagination showing 0 results when no entries exist | UI | [ ] |
|
||||||
- Report names include all selected client names
|
| 36.2 | It should show an empty table with filter pills remaining when filters match nothing | UI | [ ] |
|
||||||
|
| 36.3 | It should show an empty report form with no table rendered when report data is empty | UI | [ ] |
|
||||||
|
|
||||||
**Running balance cache**
|
### Data Locking Behaviors
|
||||||
- `refresh-running-balance-cache` recomputes balances for dirty line items
|
|
||||||
- Changing a ledger entry marks its line items and subsequent entries as dirty
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Non-dirty entries are not recomputed
|
|---|----------|---------------|--------|
|
||||||
|
| 37.1 | It should block creating journal entries for dates on or before the client's `locked-until` date | Integration | [ ] |
|
||||||
|
| 37.2 | It should reject external import entries for locked dates | Integration | [ ] |
|
||||||
|
|
||||||
|
### Unbalanced Entry Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 38.1 | It should compute debit and credit sums per entry for the "Show unbalanced" filter | Unit | [ ] |
|
||||||
|
| 38.2 | It should display unbalanced entries in the normal view without filtering | UI | [ ] |
|
||||||
|
|
||||||
|
### Account Location Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 39.1 | It should reject locations other than the fixed location for accounts with fixed locations | Integration | [ ] |
|
||||||
|
| 39.2 | It should reject "A" (all) location for accounts without location restrictions | Integration | [ ] |
|
||||||
|
| 39.3 | It should validate account location requirements on both the frontend location select and backend schema | Integration | [ ] |
|
||||||
|
|
||||||
|
### Running Balance Cache Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 40.1 | It should recompute balances for dirty line items via `refresh-running-balance-cache` | Integration | [ ] |
|
||||||
|
| 40.2 | It should mark a changed entry's line items and subsequent entries as dirty | Integration | [ ] |
|
||||||
|
| 40.3 | It should skip recomputation for non-dirty entries | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Test Data Requirements
|
## Test Data Requirements
|
||||||
|
|
||||||
**Clients**
|
| Entity | Requirements |
|
||||||
- At least 2 clients with different locations
|
|--------|-------------|
|
||||||
- Client with `locked-until` date in the past
|
| **Clients** | At least 2 clients with different locations; at least 1 client with a `locked-until` date in the past |
|
||||||
|
| **Accounts** | Asset accounts (11000-11999); Liability accounts (20000-28999); Equity accounts (30000-39999); Revenue accounts (40000-49999); Expense accounts (50000-98999); accounts with fixed locations; accounts without location restrictions |
|
||||||
|
| **Vendors** | Existing vendors; hidden vendors (auto-created on import) |
|
||||||
|
| **Journal Entries** | Balanced entries (debits = credits); unbalanced entries (debits ≠ credits); entries with multiple line items; entries linked to invoices; entries with external IDs; entries across multiple dates and locations |
|
||||||
|
| **Bank Accounts** | Bank accounts linked to clients |
|
||||||
|
|
||||||
**Accounts**
|
## Existing Tests to Preserve
|
||||||
- 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**
|
- `test/clj/auto_ap/ssr/ledger/ledger_test.clj` — Ledger page rendering and grid behaviors
|
||||||
- Existing vendors
|
- `test/clj/auto_ap/integration/routes/ledger_test.clj` — Ledger routes and mutations
|
||||||
- Hidden vendors (auto-created on import)
|
- `test/clj/auto_ap/ledger/reports_test.clj` — Report generation and calculation logic
|
||||||
|
|
||||||
**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
|
## Dependencies
|
||||||
|
|
||||||
- `auto-ap.ssr.ledger.common` - Shared grid page config, query schema, filtering
|
- `auto-ap.ssr.ledger.common` — Shared grid page config, query schema, filtering
|
||||||
- `auto-ap.ledger.reports` - Report aggregation and formatting logic
|
- `auto-ap.ledger.reports` — Report aggregation and formatting logic
|
||||||
- `auto-ap.ledger` - Running balance cache, account lookups
|
- `auto-ap.ledger` — Running balance cache, account lookups
|
||||||
- `auto-ap.datomic.accounts` - Account querying and clientization
|
- `auto-ap.datomic.accounts` — Account querying and clientization
|
||||||
- `auto-ap.permissions` - Permission checks and middleware
|
- `auto-ap.permissions` — Permission checks and middleware
|
||||||
- `auto-ap.ssr.grid-page-helper` - Generic data grid behaviors
|
- `auto-ap.ssr.grid-page-helper` — Generic data grid behaviors
|
||||||
- `auto-ap.ssr.components` - UI components (typeahead, date inputs, buttons)
|
- `auto-ap.ssr.components` — UI components (typeahead, date inputs, buttons)
|
||||||
- `auto-ap.ssr.form-cursor` - Form state management
|
- `auto-ap.ssr.form-cursor` — Form state management
|
||||||
- `clj-pdf` - PDF generation
|
- `clj-pdf` — PDF generation
|
||||||
- `amazonica.aws.s3` - S3 upload for exports
|
- `amazonica.aws.s3` — S3 upload for exports
|
||||||
- `datomic.api` - Database queries and transactions
|
- `datomic.api` — Database queries and transactions
|
||||||
|
|||||||
@@ -2,526 +2,319 @@
|
|||||||
|
|
||||||
## Overview
|
## 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**.
|
These pages are rendered client-side via Reagent/Re-frame and use GraphQL for data fetching. They are being migrated to HTMX SSR. **No UI tests should be written for these pages until migrated.**
|
||||||
|
|
||||||
### Architecture
|
**Testing Philosophy**
|
||||||
- **Routing**: Bidi client-side routes (`src/cljc/auto_ap/client_routes.cljc`)
|
- Prefer unit tests for pure business logic (calculations, validations, transformations)
|
||||||
- **State**: Re-frame subscriptions and events
|
- Use integration tests for GraphQL queries, mutations, and data flows
|
||||||
- **Data**: GraphQL via custom `graphql` effect (`auto-ap.effects`)
|
- Use UI tests only after migration to SSR; until then, mark as "UI (when migrated)"
|
||||||
- **Permissions**: `auto-ap.permissions/can?` with role-based checks
|
- Every behavior must be user-visible; no tests for implementation details
|
||||||
|
|
||||||
### 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
|
## Testing Patterns
|
||||||
|
|
||||||
### Home (`/`)
|
### Pattern: GraphQL Data Fetching
|
||||||
|
All data fetching uses re-frame `graphql` effect with JWT token from `:user` in app-db:
|
||||||
|
1. Queries use `owns-state` to track loading status per page
|
||||||
|
2. Results flow through `::data-page/received` event which stores data and syncs URL query params
|
||||||
|
3. Filter changes are debounced (800ms) via `dispatch-debounce`
|
||||||
|
|
||||||
#### Purpose
|
**Test implications:** Integration test the GraphQL query resolution and response shape. Do not test the re-frame effect machinery.
|
||||||
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
|
### Pattern: Re-frame State Management
|
||||||
1. User lands on `/`
|
- **Data pages**: `data-page` namespace provides reusable pagination/filtering state
|
||||||
2. Page loads data for currently selected client
|
- `::data-page/params` — merged filters + table params + query params
|
||||||
3. If user has access to multiple clients, shows note: "these reports are for [client]. Please choose a specific customer for their report."
|
- `::data-page/data` — GraphQL response data
|
||||||
4. User can switch cash flow range: 7/30/60/90/120/150/180 days
|
- `::data-page/checked` — selected rows for bulk operations
|
||||||
5. Cash flow bars are clickable and redirect to unpaid invoices for that date
|
- **Forms**: `forms` namespace manages edit dialogs with `start-form`, `change-handler`, `save-succeeded`
|
||||||
|
- **Status**: `status` namespace tracks async operation states (`:loading`, `:complete`, `:error`)
|
||||||
|
|
||||||
#### GraphQL Queries Used
|
**Test implications:** Test via integration tests that verify the correct data appears after state transitions. Do not test subscription internals.
|
||||||
```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)
|
### Pattern: Client-Side Routing
|
||||||
- **Happy path**: Dashboard loads with all three chart sections
|
- Navbar uses Bidi `path-for` with `routes/routes` for legacy SPA links
|
||||||
- **Multi-client note**: Displays when user hasn't selected a specific client
|
- Some navbar items (Payments, POS, Invoices) now link to `ssr-routes/only-routes` instead
|
||||||
- **Cash flow ranges**: Switching between 7-180 day views updates chart and table
|
- Page components dispatch `::mounted` on mount and `::unmounted` on unmount
|
||||||
- **Cash flow table**: Shows invoices, upcoming debits/credits with days-until
|
|
||||||
- **Edge case**: No data for client shows empty charts gracefully
|
**Test implications:** After migration, integration test route redirects from old SPA routes to new SSR routes.
|
||||||
- **Error states**: GraphQL errors show loading state appropriately
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Login (`/login`)
|
## Home
|
||||||
|
|
||||||
#### Purpose
|
### Dashboard Display
|
||||||
Authentication page with Google OAuth login.
|
|
||||||
|
|
||||||
#### User Flows
|
| # | Behavior | Test Strategy | Status |
|
||||||
1. User visits `/login`
|
|---|----------|---------------|--------|
|
||||||
2. Shows "Login with Google" button
|
| 1.1 | It should display a dashboard with three chart sections: top expense categories, upcoming bills, and cash flow projection | UI (when migrated) | [ ] |
|
||||||
3. Button links to Google OAuth with optional `redirect-to` query param
|
| 1.2 | It should load data for the currently selected client on page load | Integration | [ ] |
|
||||||
4. After auth failure/logout, may show logout reason notification
|
| 1.3 | It should display a note: "these reports are for [client]. Please choose a specific customer for their report." when the user has access to multiple clients and has not selected a specific one | UI (when migrated) | [ ] |
|
||||||
|
| 1.4 | It should display an interactive bar chart for cash flow projection | UI (when migrated) | [ ] |
|
||||||
#### GraphQL Queries/Mutations Used
|
| 1.5 | It should display a table below the cash flow chart showing invoices, upcoming debits, and upcoming credits with days-until due | UI (when migrated) | [ ] |
|
||||||
None.
|
| 1.6 | It should allow switching the cash flow range between 7, 30, 60, 90, 120, 150, and 180 days | UI (when migrated) | [ ] |
|
||||||
|
| 1.7 | Given the user switches the cash flow range, then the chart and table should update to reflect the selected range | Integration | [ ] |
|
||||||
#### Behaviors to Test (when migrated)
|
| 1.8 | Given the user clicks a cash flow bar, then it should redirect to the unpaid invoices page for that date | UI (when migrated) | [ ] |
|
||||||
- **Happy path**: Shows login button linking to Google OAuth
|
| 1.9 | It should show empty charts gracefully when there is no data for the selected client | UI (when migrated) | [ ] |
|
||||||
- **Redirect**: Preserves `redirect-to` query parameter in OAuth URL
|
| 1.10 | It should show a loading state while GraphQL data is being fetched | UI (when migrated) | [ ] |
|
||||||
- **Logout reason**: Displays warning notification when `logout-reason` is set
|
| 1.11 | It should handle GraphQL errors by showing the loading state appropriately | UI (when migrated) | [ ] |
|
||||||
- **Edge case**: Already authenticated user visiting login page
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Needs Activation (`/needs-activation`)
|
## Login
|
||||||
|
|
||||||
#### Purpose
|
### Authentication
|
||||||
Shown when authenticated user's account is not yet activated.
|
|
||||||
|
|
||||||
#### User Flows
|
| # | Behavior | Test Strategy | Status |
|
||||||
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."
|
| 2.1 | It should display a "Login with Google" button on the login page | UI (when migrated) | [ ] |
|
||||||
3. "here" link clears user state and redirects to login
|
| 2.2 | It should link the login button to Google OAuth | UI (when migrated) | [ ] |
|
||||||
|
| 2.3 | It should preserve the `redirect-to` query parameter in the Google OAuth URL | Integration | [ ] |
|
||||||
|
| 2.4 | It should display a warning notification with the logout reason when the `logout-reason` query parameter is set | UI (when migrated) | [ ] |
|
||||||
|
| 2.5 | It should redirect an already authenticated user away from the login page | Integration | [ ] |
|
||||||
|
|
||||||
#### GraphQL Queries/Mutations Used
|
### Needs Activation
|
||||||
None.
|
|
||||||
|
|
||||||
#### Behaviors to Test (when migrated)
|
| # | Behavior | Test Strategy | Status |
|
||||||
- **Happy path**: Shows activation required message
|
|---|----------|---------------|--------|
|
||||||
- **Relogin link**: Clears user state and redirects to login
|
| 2.6 | It should display the message: "Sorry, your user is not activated yet. Please have Ben Skinner enable your account." when the user's account is inactive | UI (when migrated) | [ ] |
|
||||||
|
| 2.7 | It should provide a "here" link that clears user state and redirects to the login page | Integration | [ ] |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Transactions (`/transactions/`, `/transactions/unapproved`, `/transactions/approved`, `/transactions/requires-feedback`, `/transactions/excluded`)
|
## Transactions
|
||||||
|
|
||||||
#### Purpose
|
### List Display
|
||||||
Transaction list with filtering, editing, and bulk admin operations. Different routes show different approval statuses.
|
|
||||||
|
|
||||||
#### User Flows
|
| # | Behavior | Test Strategy | Status |
|
||||||
1. User navigates to transactions page
|
|---|----------|---------------|--------|
|
||||||
2. Default date filter: last 1 month
|
| 3.1 | It should display a table of transactions with columns for amount, memo, location, approval status, vendor, accounts, date, and description | UI (when migrated) | [ ] |
|
||||||
3. Side bar allows filtering by: vendor, account, bank account, date range, amount range, location, import batch, description, linked status
|
| 3.2 | It should load the transaction list with a default date filter of the last 1 month | Integration | [ ] |
|
||||||
4. Table shows transactions with checkbox selection (admin only)
|
| 3.3 | It should allow filtering by vendor, account, bank account, date range, amount range, location, import batch, description, and linked status via a sidebar | UI (when migrated) | [ ] |
|
||||||
5. Clicking row opens edit side bar
|
| 3.4 | It should debounce sidebar filter changes by 800ms before refreshing data | Integration | [ ] |
|
||||||
6. Admin can: bulk code, bulk delete, suppress, or manual Yodlee import
|
| 3.5 | It should support pagination with start and per-page parameters | Integration | [ ] |
|
||||||
|
| 3.6 | It should display checkboxes for row selection when the user is an admin | UI (when migrated) | [ ] |
|
||||||
|
| 3.7 | It should not display checkboxes or bulk action buttons for non-admin users | UI (when migrated) | [ ] |
|
||||||
|
|
||||||
#### GraphQL Queries/Mutations Used
|
### Transaction Edit
|
||||||
```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) {
|
| # | Behavior | Test Strategy | Status |
|
||||||
edit_transaction(transaction: $transaction) { ... }
|
|---|----------|---------------|--------|
|
||||||
}
|
| 3.8 | It should open an edit sidebar when the user clicks a transaction row | UI (when migrated) | [ ] |
|
||||||
|
| 3.9 | It should allow updating the vendor, approval status, memo, and expense accounts | UI (when migrated) | [ ] |
|
||||||
|
| 3.10 | It should validate that the sum of expense account amounts equals the transaction amount | Unit + Integration | [ ] |
|
||||||
|
| 3.11 | It should validate that locations are valid for the selected accounts | Unit + Integration | [ ] |
|
||||||
|
| 3.12 | It should block editing locked transactions | Integration | [ ] |
|
||||||
|
| 3.13 | It should display validation errors inline in the edit form | UI (when migrated) | [ ] |
|
||||||
|
|
||||||
mutation DeleteTransactions($filters: transaction_filters, $ids: [id], $suppress: Boolean) {
|
### Bulk Operations (Admin)
|
||||||
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]) {
|
| # | Behavior | Test Strategy | Status |
|
||||||
bulk_code_transactions(filters: $filters, ids: $ids, vendor: $vendor, approval_status: $approval_status, accounts: $accounts) { message }
|
|---|----------|---------------|--------|
|
||||||
}
|
| 3.14 | It should allow deleting selected transactions or all visible transactions | Integration | [ ] |
|
||||||
```
|
| 3.15 | It should allow suppressing selected transactions instead of deleting them | Integration | [ ] |
|
||||||
|
| 3.16 | It should allow bulk coding by applying vendor, account, approval status, and account rules to multiple transactions | Integration | [ ] |
|
||||||
|
| 3.17 | It should allow importing transactions via a manual Yodlee import dialog | Integration | [ ] |
|
||||||
|
|
||||||
#### Behaviors to Test (when migrated)
|
### Route Variants
|
||||||
- **Happy path**: Loads transaction list with default 1-month filter
|
|
||||||
- **Route variants**: Each approval-status route applies correct default filter
|
| # | Behavior | Test Strategy | Status |
|
||||||
- **Filtering**: Side bar filters trigger debounced data refresh (800ms)
|
|---|----------|---------------|--------|
|
||||||
- **Pagination**: Start/per-page params work correctly
|
| 3.18 | It should apply the correct default approval status filter for each route variant: all, unapproved, approved, requires-feedback, excluded | Integration | [ ] |
|
||||||
- **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/`)
|
## Ledger
|
||||||
|
|
||||||
#### Purpose
|
### General Ledger
|
||||||
General ledger showing journal entries with line items. Supports filtering and CSV export.
|
|
||||||
|
|
||||||
#### User Flows
|
| # | Behavior | Test Strategy | Status |
|
||||||
1. User navigates to `/ledger/`
|
|---|----------|---------------|--------|
|
||||||
2. Default date filter: last 1 month
|
| 4.1 | It should display a table of journal entries with expandable line items | UI (when migrated) | [ ] |
|
||||||
3. Side bar filters: vendor, account, bank account, date range, amount range, location
|
| 4.2 | It should load the ledger with a default date filter of the last 1 month | Integration | [ ] |
|
||||||
4. Table shows journal entries with expandable line items
|
| 4.3 | It should allow filtering by vendor, account, bank account, date range, amount range, and location via a sidebar | UI (when migrated) | [ ] |
|
||||||
5. Admin can export to CSV
|
| 4.4 | It should support virtual pagination controls | Integration | [ ] |
|
||||||
|
| 4.5 | It should allow admin users to export filtered results as a CSV download | Integration | [ ] |
|
||||||
|
| 4.6 | It should display "Not authorized" for users with the manager role | UI (when migrated) | [ ] |
|
||||||
|
|
||||||
#### GraphQL Queries/Mutations Used
|
### Profit and Loss
|
||||||
```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) {
|
| # | Behavior | Test Strategy | Status |
|
||||||
ledger_csv(filters: $filters) { csv_content_b64 }
|
|---|----------|---------------|--------|
|
||||||
}
|
| 4.7 | It should generate a profit and loss report for a single client with a default period | Integration | [ ] |
|
||||||
```
|
| 4.8 | It should allow selecting multiple companies via a multi-select typeahead and generating a combined report | UI (when migrated) | [ ] |
|
||||||
|
| 4.9 | It should provide period preset buttons: 13 periods, 12 months, last week, week-to-date, last month, month-to-date, year-to-date, last calendar year, and full year | UI (when migrated) | [ ] |
|
||||||
|
| 4.10 | It should populate the correct date ranges when a period preset is selected | Unit | [ ] |
|
||||||
|
| 4.11 | It should allow custom period selection via start and end date pickers in advanced mode | UI (when migrated) | [ ] |
|
||||||
|
| 4.12 | It should optionally include period-over-period deltas | UI (when migrated) | [ ] |
|
||||||
|
| 4.13 | It should optionally break out the report by location, which is mutually exclusive with including deltas | Integration | [ ] |
|
||||||
|
| 4.14 | It should allow admin users to generate and download a PDF export | Integration | [ ] |
|
||||||
|
| 4.15 | It should provide an email composition link for single-client PDF exports | UI (when migrated) | [ ] |
|
||||||
|
| 4.16 | It should open a ledger detail sidebar when the user clicks a report cell | UI (when migrated) | [ ] |
|
||||||
|
| 4.17 | It should display "Not authorized" for users with the manager role | UI (when migrated) | [ ] |
|
||||||
|
|
||||||
#### Behaviors to Test (when migrated)
|
### Cash Flows
|
||||||
- **Happy path**: Loads ledger with default 1-month filter
|
|
||||||
- **Filtering**: Date range, vendor, account, location filters work
|
| # | Behavior | Test Strategy | Status |
|
||||||
- **CSV export**: Admin can export filtered results as base64 CSV download
|
|---|----------|---------------|--------|
|
||||||
- **Pagination**: Virtual pagination controls
|
| 4.18 | It should generate a cash flows statement report | Integration | [ ] |
|
||||||
- **Permissions**: Manager role sees "Not authorized"
|
| 4.19 | It should use the same company, period, delta, and location column controls as the profit and loss report | Integration | [ ] |
|
||||||
|
| 4.20 | It should allow admin users to export the report to PDF | Integration | [ ] |
|
||||||
|
| 4.21 | It should open a ledger detail sidebar when the user clicks a report cell | UI (when migrated) | [ ] |
|
||||||
|
| 4.22 | It should display "Not authorized" for users with the manager role | UI (when migrated) | [ ] |
|
||||||
|
|
||||||
|
### Profit and Loss Detail
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 4.23 | It should generate a detailed journal entry report with a default 2-week date range | Integration | [ ] |
|
||||||
|
| 4.24 | It should group journal entries by category: sales, COGS, payroll, controllable, fixed overhead, and ownership controllable | Integration | [ ] |
|
||||||
|
| 4.25 | It should display Gross Profit, Overhead, and Net Profit summaries | UI (when migrated) | [ ] |
|
||||||
|
| 4.26 | It should filter journal entries by the selected start and end dates | Integration | [ ] |
|
||||||
|
| 4.27 | It should allow admin users to export the report to PDF with an email link for single client | Integration | [ ] |
|
||||||
|
| 4.28 | It should display "Not authorized" for users with the manager role | UI (when migrated) | [ ] |
|
||||||
|
|
||||||
|
### Balance Sheet
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 4.29 | It should generate a balance sheet report as of a specific date | Integration | [ ] |
|
||||||
|
| 4.30 | It should allow selecting multiple companies and combining them into a single report | Integration | [ ] |
|
||||||
|
| 4.31 | It should optionally include a prior-year comparison with a side-by-side view | UI (when migrated) | [ ] |
|
||||||
|
| 4.32 | It should allow admin users to export the report to PDF with an email composition link for single client | Integration | [ ] |
|
||||||
|
| 4.33 | It should open ledger entries filtered by account and date range when the user clicks a cell | UI (when migrated) | [ ] |
|
||||||
|
| 4.34 | It should display "Not authorized" for users with the manager role | UI (when migrated) | [ ] |
|
||||||
|
|
||||||
|
### External Ledger
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 4.35 | It should display only externally-imported journal entries with an external_id | Integration | [ ] |
|
||||||
|
| 4.36 | It should allow admin users to delete selected entries, with a maximum of 1000 at once | Integration | [ ] |
|
||||||
|
| 4.37 | It should allow admin users to export to CSV | Integration | [ ] |
|
||||||
|
| 4.38 | It should display "Not authorized" for non-admin users | UI (when migrated) | [ ] |
|
||||||
|
|
||||||
|
### External Import
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 4.39 | It should allow admin users to paste tab-separated data into a textarea | UI (when migrated) | [ ] |
|
||||||
|
| 4.40 | It should provide a checkbox to indicate whether the first row is a header | UI (when migrated) | [ ] |
|
||||||
|
| 4.41 | It should parse the pasted data into a table with columns: Id, Client, Source, Vendor, Date, Account, Location, Debit, Credit, Note, and Cleared against | Integration | [ ] |
|
||||||
|
| 4.42 | It should validate that the client code exists | Unit + Integration | [ ] |
|
||||||
|
| 4.43 | It should validate that the vendor exists | Unit + Integration | [ ] |
|
||||||
|
| 4.44 | It should validate that the date is in MM/dd/yyyy format | Unit + Integration | [ ] |
|
||||||
|
| 4.45 | It should validate that total debits equal total credits | Unit + Integration | [ ] |
|
||||||
|
| 4.46 | It should validate that all amounts are greater than 0 | Unit + Integration | [ ] |
|
||||||
|
| 4.47 | It should validate that entries are dated after the client's locked-until date | Integration | [ ] |
|
||||||
|
| 4.48 | It should validate that the location belongs to the client or is "A" | Unit + Integration | [ ] |
|
||||||
|
| 4.49 | It should validate that the account exists and the location matches the account's required location | Unit + Integration | [ ] |
|
||||||
|
| 4.50 | It should display errors per row with a dropdown explanation | UI (when migrated) | [ ] |
|
||||||
|
| 4.51 | It should show status icons indicating success, ignored, or existing per row after import | UI (when migrated) | [ ] |
|
||||||
|
| 4.52 | It should display the total success count, ignored count, and error count after import | UI (when migrated) | [ ] |
|
||||||
|
| 4.53 | It should provide an "Only show errors" filter | UI (when migrated) | [ ] |
|
||||||
|
| 4.54 | It should display "Not authorized" for non-admin users | UI (when migrated) | [ ] |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Profit and Loss (`/ledger/profit-and-loss`)
|
## Payments
|
||||||
|
|
||||||
#### Purpose
|
| # | Behavior | Test Strategy | Status |
|
||||||
Financial report showing revenue and expenses over configurable periods. Supports multi-company and PDF export.
|
|---|----------|---------------|--------|
|
||||||
|
| 5.1 | It should be fully migrated to SSR at `/payment/`; the legacy client route exists only for navbar highlighting | N/A | [x] |
|
||||||
#### 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`)
|
## Reports
|
||||||
|
|
||||||
#### Purpose
|
| # | Behavior | Test Strategy | Status |
|
||||||
Statement of cash flows report. Same control structure as P&L but different report format.
|
|---|----------|---------------|--------|
|
||||||
|
| 6.1 | It should be fully migrated to SSR at `/company/reports`; the legacy client route exists only for navbar highlighting | N/A | [x] |
|
||||||
#### 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`)
|
## Vendors
|
||||||
|
|
||||||
#### Purpose
|
### Admin Vendor Management
|
||||||
Detailed journal entry report broken down by category (sales, COGS, payroll, controllable, fixed overhead, ownership controllable).
|
|
||||||
|
|
||||||
#### User Flows
|
| # | Behavior | Test Strategy | Status |
|
||||||
1. Select companies
|
|---|----------|---------------|--------|
|
||||||
2. Select date range (start/end date pickers)
|
| 7.1 | It should be fully migrated to SSR at `/admin/vendor`; the legacy client route exists only for navbar highlighting | N/A | [x] |
|
||||||
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
|
### New Vendor Dialog
|
||||||
```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]) {
|
| # | Behavior | Test Strategy | Status |
|
||||||
journal_detail_report_pdf(...) { url name }
|
|---|----------|---------------|--------|
|
||||||
}
|
| 7.2 | It should load the home dashboard with the vendor creation dialog pre-opened when navigating to `/vendor/new` | UI (when migrated) | [ ] |
|
||||||
```
|
| 7.3 | It should only open the vendor dialog if the user has `:vendor :create` permission | Integration | [ ] |
|
||||||
|
| 7.4 | It should allow creating a new vendor with name, terms, address, contacts, and default account | UI (when migrated) | [ ] |
|
||||||
#### Behaviors to Test (when migrated)
|
| 7.5 | It should validate that only one terms override exists per client | Unit + Integration | [ ] |
|
||||||
- **Happy path**: Generate detail report with default 2-week range
|
| 7.6 | It should validate that only one schedule payment DOM override exists per client | Unit + Integration | [ ] |
|
||||||
- **Category breakdown**: Report includes all 6 category sections plus Gross Profit, Overhead, Net Profit summaries
|
| 7.7 | It should validate that only one account override exists per client | Unit + Integration | [ ] |
|
||||||
- **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
|
## Cross-Cutting Behaviors
|
||||||
|
|
||||||
### GraphQL Query Patterns
|
### 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)
|
| # | Behavior | Test Strategy | Status |
|
||||||
- **Data pages**: `data-page` namespace provides reusable pagination/filtering state
|
|---|----------|---------------|--------|
|
||||||
- `::data-page/params` - merged filters + table params + query params
|
| 8.1 | It should include the JWT token from the `:user` app-db state in all GraphQL requests | Integration | [ ] |
|
||||||
- `::data-page/data` - GraphQL response data
|
| 8.2 | It should track loading status per page using `owns-state` | Integration | [ ] |
|
||||||
- `::data-page/checked` - selected rows for bulk operations
|
| 8.3 | It should store GraphQL response data and sync URL query params via the `::data-page/received` event | Integration | [ ] |
|
||||||
- **Forms**: `forms` namespace manages edit dialogs with `start-form`, `change-handler`, `save-succeeded`
|
| 8.4 | It should debounce filter changes by 800ms before dispatching GraphQL requests | Integration | [ ] |
|
||||||
- **Status**: `status` namespace tracks async operation states (`:loading`, `:complete`, `:error`)
|
|
||||||
|
|
||||||
### Navigation Between Legacy Pages
|
### Re-frame State Management
|
||||||
- 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
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Page components dispatch `::mounted` on mount and `::unmounted` on unmount to set up/tear down:
|
|---|----------|---------------|--------|
|
||||||
- Data subscriptions
|
| 8.5 | It should merge filters, table params, and query params into `::data-page/params` | Integration | [ ] |
|
||||||
- Forward event listeners
|
| 8.6 | It should store GraphQL response data in `::data-page/data` | Integration | [ ] |
|
||||||
- Track subscriptions (parameter change watchers)
|
| 8.7 | It should track selected rows for bulk operations in `::data-page/checked` | Integration | [ ] |
|
||||||
|
| 8.8 | It should manage edit dialog state with `start-form`, `change-handler`, and `save-succeeded` events | Integration | [ ] |
|
||||||
|
| 8.9 | It should track async operation states as `:loading`, `:complete`, or `:error` | Integration | [ ] |
|
||||||
|
|
||||||
|
### Navigation
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 8.10 | It should redirect old SPA routes to new SSR routes after migration | Integration | [ ] |
|
||||||
|
| 8.11 | It should set up data subscriptions, forward event listeners, and parameter change watchers on page mount | Integration | [ ] |
|
||||||
|
| 8.12 | It should tear down data subscriptions, forward event listeners, and parameter change watchers on page unmount | Integration | [ ] |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Test Data Requirements
|
||||||
|
|
||||||
|
| Entity | Requirements |
|
||||||
|
|--------|-------------|
|
||||||
|
| **Users** | Admin, power-user, manager, user, and read-only roles |
|
||||||
|
| **Clients** | Multiple clients with different locations; some with locked-until dates |
|
||||||
|
| **Vendors** | With/without default accounts, terms, and autopay settings |
|
||||||
|
| **Accounts** | Expense accounts with/without invoice allowance; different locations |
|
||||||
|
| **Bank Accounts** | Check, cash, and credit types |
|
||||||
|
| **Transactions** | Various approval statuses, dates, amounts, locked/unlocked states |
|
||||||
|
| **Journal Entries** | With/without external_id; various categories and accounts |
|
||||||
|
| **Invoices** | Various statuses and due dates for cash flow projections |
|
||||||
|
|
||||||
|
## Existing Tests to Preserve
|
||||||
|
|
||||||
|
- Test GraphQL query resolution for `HomeDashboard`, `TransactionPage`, `LedgerPage`, `ProfitAndLoss`, `CashFlows`, `JournalDetailReport`, `BalanceSheet`, and `ExternalLedger`
|
||||||
|
- Test re-frame event handlers for data page state transitions
|
||||||
|
- Test form validation logic for transaction editing and external ledger import
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- GraphQL API (data fetching)
|
||||||
|
- Re-frame/Reagent (client-side state and rendering)
|
||||||
|
- Bidi (client-side routing)
|
||||||
|
- Recharts (chart rendering on Home page)
|
||||||
|
- Server-side PDF generation (P&L, Cash Flows, Balance Sheet, P&L Detail)
|
||||||
|
|
||||||
## Migration Notes
|
## Migration Notes
|
||||||
|
|
||||||
### Actively Migrated / Already SSR
|
### Already Migrated to SSR
|
||||||
- **Payments** (`/payments/`) - Fully migrated to `/payment/` SSR routes
|
- **Payments** (`/payments/`) - Fully migrated to `/payment/` SSR routes
|
||||||
- **Reports** (`/reports/`) - Fully migrated to `/company/reports` SSR routes
|
- **Reports** (`/reports/`) - Fully migrated to `/company/reports` SSR routes
|
||||||
- **Admin Vendors** (`/admin/vendors`) - Fully migrated to `/admin/vendor` SSR routes
|
- **Admin Vendors** (`/admin/vendors`) - Fully migrated to `/admin/vendor` SSR routes
|
||||||
|
|||||||
@@ -4,176 +4,189 @@
|
|||||||
|
|
||||||
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.
|
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
|
**Testing Philosophy**
|
||||||
|
- Prefer unit tests for pure business logic (calculations, validations, transformations)
|
||||||
|
- Use integration tests for database interactions and cross-system flows
|
||||||
|
- Use UI tests only for end-to-end happy paths that touch multiple pages
|
||||||
|
- Every behavior must be user-visible; no tests for implementation details
|
||||||
|
|
||||||
| Route | Method | Handler | 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
|
## Testing Patterns
|
||||||
|
|
||||||
### Unit Tests
|
### Pattern: Form Submission with HTMX
|
||||||
|
Outgoing invoice forms use HTMX for asynchronous submission and partial page updates:
|
||||||
|
1. Form fields are rendered server-side with validation state
|
||||||
|
2. HTMX handles POST submission and swaps the response into the page
|
||||||
|
3. Success responses trigger modal display with PDF download link
|
||||||
|
4. Error responses re-render the form with validation errors
|
||||||
|
|
||||||
- `form-schema` validates required fields: client, date, to, invoice-number, tax, to-address, line-items
|
**Test implications:** Unit test validation logic and calculation functions. Integration test the full POST flow. UI test only the happy path.
|
||||||
- `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
|
### Pattern: Dynamic Line Items
|
||||||
|
Line items are added and removed dynamically without page reload:
|
||||||
|
1. "Add line" button fetches a new row via HTMX
|
||||||
|
2. Each row has description, quantity, unit price inputs, and a delete button
|
||||||
|
3. Delete uses Alpine.js to fade out and remove the row
|
||||||
|
4. Empty line items are filtered out on submission
|
||||||
|
|
||||||
- `GET /outgoing-invoice/new` returns 200 with full form HTML for authenticated user
|
**Test implications:** Unit test the filtering and calculation logic. Integration test HTMX endpoints. UI test the add/remove interactions.
|
||||||
- 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
|
## Create 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
|
### Form Display Behaviors
|
||||||
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
|
| # | Behavior | Test Strategy | Status |
|
||||||
1. User clicks "Generate" without filling required fields
|
|---|----------|---------------|--------|
|
||||||
2. Form POSTs and returns with validation error styling
|
| 1.1 | It should render a new invoice form with breadcrumbs: Invoices > Outgoing > New | UI | [ ] |
|
||||||
3. Required fields show error indicators
|
| 1.2 | It should display a client typeahead field labeled "From (client)" | UI | [ ] |
|
||||||
4. User fills in missing fields and resubmits
|
| 1.3 | It should display an invoice number input field | UI | [ ] |
|
||||||
5. Invoice generates successfully
|
| 1.4 | It should display a date picker pre-filled with today's date in `normal-date` format | UI | [ ] |
|
||||||
|
| 1.5 | It should display recipient name "To" field | UI | [ ] |
|
||||||
|
| 1.6 | It should display recipient address fields: street1, street2, city, state, zip | UI | [ ] |
|
||||||
|
| 1.7 | It should display a line items grid with one default empty row | UI | [ ] |
|
||||||
|
| 1.8 | It should display a tax percentage input with default value 10.0 | UI | [ ] |
|
||||||
|
| 1.9 | It should display a "Generate" button to submit the form | UI | [ ] |
|
||||||
|
| 1.10 | It should display an "Add line" button to add more line items | UI | [ ] |
|
||||||
|
|
||||||
#### Empty Line Items Handling
|
### Form Validation Behaviors
|
||||||
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
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 2.1 | It should require client selection | Integration | [ ] |
|
||||||
|
| 2.2 | It should require invoice date | Integration | [ ] |
|
||||||
|
| 2.3 | It should require recipient name in "To" field | Integration | [ ] |
|
||||||
|
| 2.4 | It should require invoice number | Integration | [ ] |
|
||||||
|
| 2.5 | It should require at least one line item with description, quantity, and unit price | Integration | [ ] |
|
||||||
|
| 2.6 | It should make recipient address street2 optional | Unit | [ ] |
|
||||||
|
| 2.7 | It should strip whitespace from street2 and treat empty as nil | Unit | [ ] |
|
||||||
|
| 2.8 | It should coerce line items from nested form parameters into a vector | Unit | [ ] |
|
||||||
|
| 2.9 | It should display validation errors next to the offending fields | UI | [ ] |
|
||||||
|
| 2.10 | It should redisplay the form with entered data preserved when validation fails | Integration | [ ] |
|
||||||
|
|
||||||
### Form Validation
|
### Submission Behaviors
|
||||||
- **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
|
| # | Behavior | Test Strategy | Status |
|
||||||
- **All line items empty**: Subtotal is `0.0`, tax is `0.0`, total is `0.0`
|
|---|----------|---------------|--------|
|
||||||
- **Single line item**: Calculates correctly
|
| 3.1 | It should filter out line items with empty descriptions before calculation | Unit | [ ] |
|
||||||
- **Many line items (50+)**: Grid scrolls; each item calculates independently
|
| 3.2 | It should calculate each line item total as `unit-price * quantity` | Unit | [ ] |
|
||||||
- **Delete all line items**: Grid empty; adding new row restores functionality
|
| 3.3 | It should calculate subtotal as the sum of all line item totals | Unit | [ ] |
|
||||||
- **Line item with only description**: Filtered out (requires description + values)
|
| 3.4 | It should calculate tax as `subtotal * (tax-rate / 100)` | Unit | [ ] |
|
||||||
|
| 3.5 | It should calculate total as `subtotal + tax` | Unit | [ ] |
|
||||||
|
| 3.6 | It should format monetary values as `$X,XXX.XX` strings before sending to Lambda | Unit | [ ] |
|
||||||
|
| 3.7 | It should format the invoice date as `normal-date` string before sending to Lambda | Unit | [ ] |
|
||||||
|
| 3.8 | It should invoke the `genpdf` Lambda function with a JSON payload | Integration | [ ] |
|
||||||
|
| 3.9 | It should extract the S3 URL from the Lambda response | Integration | [ ] |
|
||||||
|
| 3.10 | It should display a modal with "Download your invoice" and a link to the S3 URL | UI | [ ] |
|
||||||
|
| 3.11 | Given the Lambda invocation fails, then it should display an error without showing a modal | Integration | [ ] |
|
||||||
|
| 3.12 | Given all line items are empty, then subtotal should be `0.0`, tax should be `0.0`, and total should be `0.0` | Unit | [ ] |
|
||||||
|
|
||||||
### 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
|
## Line Items
|
||||||
- **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
|
### Add Line Item Behaviors
|
||||||
- **Tax as whole number (10)**: Treated as 10% (multiplied by 0.10)
|
|
||||||
- **Tax with decimals (8.25)**: Treated as 8.25%
|
| # | Behavior | Test Strategy | Status |
|
||||||
- **Tax over 100%**: Allowed by schema; mathematically valid but business-questionable
|
|---|----------|---------------|--------|
|
||||||
- **Zero tax**: Total equals subtotal exactly
|
| 4.1 | It should fetch a new empty line item row via HTMX when "Add line" is clicked | Integration | [ ] |
|
||||||
|
| 4.2 | It should append the new row to the line items grid | UI | [ ] |
|
||||||
|
| 4.3 | It should render each row with hidden db/id, description input, quantity money-input, unit-price money-input, and delete button | UI | [ ] |
|
||||||
|
| 4.4 | It should allow adding multiple line items | UI | [ ] |
|
||||||
|
|
||||||
|
### Remove Line Item Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 5.1 | It should fade out a line item row over 500ms when the delete button is clicked | UI | [ ] |
|
||||||
|
| 5.2 | It should remove the line item row from the DOM after the fade animation | UI | [ ] |
|
||||||
|
| 5.3 | It should preserve data in remaining line items after deletion | UI | [ ] |
|
||||||
|
| 5.4 | It should allow deleting all line items, leaving the grid empty | UI | [ ] |
|
||||||
|
|
||||||
|
### Calculation Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 6.1 | It should handle negative quantities in line item calculations | Unit | [ ] |
|
||||||
|
| 6.2 | It should show `$0.00` for line items with zero unit price | Unit | [ ] |
|
||||||
|
| 6.3 | It should format large monetary values with comma separators (e.g., `$1,234.56`) | Unit | [ ] |
|
||||||
|
| 6.4 | It should format nil monetary values as `$0.00` | Unit | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PDF Generation
|
||||||
|
|
||||||
|
### Lambda Integration Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 7.1 | It should invoke `genpdf` Lambda with a JSON payload containing invoice data | Integration | [ ] |
|
||||||
|
| 7.2 | It should include formatted monetary strings in the Lambda payload | Unit | [ ] |
|
||||||
|
| 7.3 | It should include the invoice date as a `normal-date` string in the Lambda payload | Unit | [ ] |
|
||||||
|
| 7.4 | It should extract the S3 URL from a successful Lambda response | Integration | [ ] |
|
||||||
|
| 7.5 | It should present the S3 URL as a clickable download link in the modal | UI | [ ] |
|
||||||
|
|
||||||
|
### Error Handling Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 8.1 | Given the Lambda returns invalid JSON, then it should propagate an error | Integration | [ ] |
|
||||||
|
| 8.2 | Given the S3 URL is inaccessible, then the link should still be presented but may fail on click | UI | [ ] |
|
||||||
|
| 8.3 | Given a very large invoice payload, then Lambda payload size limits may apply | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-Cutting Behaviors
|
||||||
|
|
||||||
|
### Authentication Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 9.1 | It should redirect unauthenticated users to `/login` | Integration | [ ] |
|
||||||
|
| 9.2 | It should redirect unauthenticated users back to `/outgoing-invoice/new` after login | Integration | [ ] |
|
||||||
|
| 9.3 | It should apply `wrap-secure` middleware to all routes | Integration | [ ] |
|
||||||
|
| 9.4 | It should apply `wrap-trim-client-ids` middleware to requests | Integration | [ ] |
|
||||||
|
|
||||||
|
### Client Selection Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 10.1 | It should populate the client typeahead from the `:company-search` endpoint | Integration | [ ] |
|
||||||
|
| 10.2 | It should only show clients the authenticated user has access to | Integration | [ ] |
|
||||||
|
|
||||||
|
### Tax Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 11.1 | It should treat a whole number tax (e.g., 10) as 10% | Unit | [ ] |
|
||||||
|
| 11.2 | It should treat a decimal tax (e.g., 8.25) as 8.25% | Unit | [ ] |
|
||||||
|
| 11.3 | It should allow tax rates over 100% | Unit | [ ] |
|
||||||
|
| 11.4 | It should calculate total equal to subtotal when tax is zero | Unit | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Test Data Requirements
|
## Test Data Requirements
|
||||||
|
|
||||||
### Users
|
| Entity | Requirements |
|
||||||
- Authenticated user with access to at least one client
|
|--------|-------------|
|
||||||
- Client with complete profile including address
|
| **Users** | Authenticated user with access to at least one client |
|
||||||
|
| **Clients** | Multiple clients with complete profiles including address (name, street, city, state, zip) |
|
||||||
|
| **Form Data** | Valid invoice number strings; valid dates in `normal-date` format; recipient names and addresses |
|
||||||
|
| **Line Items** | Descriptions, quantities (numeric), unit prices (monetary) |
|
||||||
|
| **Tax Rates** | Percentage values (e.g., 10.0 for 10%) |
|
||||||
|
| **AWS Lambda** | Mock `genpdf` Lambda returning valid S3 URL; mock `genpdf` Lambda returning error |
|
||||||
|
|
||||||
### Clients
|
## Existing Tests to Preserve
|
||||||
- Client with `:client/name` and `:client/address` (street, city, state, zip)
|
|
||||||
- Multiple clients to test typeahead selection
|
|
||||||
|
|
||||||
### Form Data
|
- `test/clj/auto_ap/ssr/outgoing_invoice_test.clj` — Outgoing invoice form rendering, submission, and PDF generation
|
||||||
- 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
|
## Dependencies
|
||||||
|
|
||||||
### External Services
|
- Datomic (client lookup for typeahead, address data)
|
||||||
- **AWS Lambda**: `genpdf` function generates PDF from invoice data
|
- AWS Lambda (`genpdf` function generates PDF from invoice data)
|
||||||
- **S3**: Generated PDFs stored at `data.prod.app.integreatconsult.com/<path>`
|
- AWS S3 (generated PDFs stored at `data.prod.app.integreatconsult.com/<path>`)
|
||||||
- **Datomic**: Client lookup for typeahead, address data
|
- HTMX (form submission, line item row fetching)
|
||||||
|
- Alpine.js (line item row removal animation)
|
||||||
### Frontend Libraries
|
- Form cursor (`auto-ap.ssr.form-cursor`) — field state management, error binding
|
||||||
- **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
|
|
||||||
|
|||||||
@@ -2,226 +2,250 @@
|
|||||||
|
|
||||||
## Overview
|
## 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.
|
Payments represent disbursements to vendors for invoices. The payment system supports multiple payment types (check, cash, debit, 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
|
**Testing Philosophy**
|
||||||
|
- Prefer unit tests for pure business logic (calculations, validations, transformations)
|
||||||
|
- Use integration tests for database interactions and cross-system flows
|
||||||
|
- Use UI tests only for end-to-end happy paths that touch multiple pages
|
||||||
|
- Every behavior must be user-visible; no tests for implementation details
|
||||||
|
|
||||||
| Route | 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
|
## Testing Patterns
|
||||||
|
|
||||||
### Payment List
|
### Pattern: Grid Page Behaviors
|
||||||
|
Most list pages in Integreat follow the same pattern:
|
||||||
|
1. Fetch IDs via Datomic query with filters
|
||||||
|
2. Hydrate results via `pull-many`
|
||||||
|
3. Render table with sortable columns
|
||||||
|
4. Support selection (individual / all / all-filtered)
|
||||||
|
5. Action buttons appear conditionally based on permissions and selection state
|
||||||
|
|
||||||
**Grid Display**
|
**Test implications:** Validate the query generation, not the rendering. UI tests only need to verify one filter and one sort work end-to-end.
|
||||||
- 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**
|
### Pattern: Permission Gates
|
||||||
- `cleared` — primary-colored pill
|
Every mutating operation checks:
|
||||||
- `pending` — secondary-colored pill
|
1. `assert-can-see-client` — user has access to the client
|
||||||
- `voided` — red-colored pill
|
2. `assert-not-locked` — payment date >= client locked-until
|
||||||
|
3. `can?` — user has the specific permission for the activity
|
||||||
|
|
||||||
**Check Number Links**
|
**Test implications:** Integration test each gate independently. UI tests only verify the happy path with a permitted user.
|
||||||
- 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**
|
### Pattern: Check Generation Behaviors
|
||||||
- Shows dropdown with links to associated invoices ("Inv. #{number}")
|
Check printing involves:
|
||||||
- Shows link to associated transaction if one exists ("Transaction")
|
1. Validation of invoices and bank account
|
||||||
|
2. Sequential check number assignment
|
||||||
|
3. PDF generation with MICR encoding
|
||||||
|
4. S3 upload and storage
|
||||||
|
5. Transaction creation (for cash payments)
|
||||||
|
|
||||||
**Action Buttons**
|
**Test implications:** Integration test the full flow once. Unit test validation logic and PDF content generation.
|
||||||
- "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
|
## Payment List Page
|
||||||
|
|
||||||
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.
|
### Display Behaviors
|
||||||
|
|
||||||
### Bulk Delete (Void)
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 1.1 | It should display a table with columns: Client, Vendor, Bank Account, Check #, Status, Date, Amount, Links | UI | [ ] |
|
||||||
|
| 1.2 | It should show the Client column only when viewing payments for multiple clients | Integration | [ ] |
|
||||||
|
| 1.3 | It should hide the Bank Account and Date columns on smaller viewports | UI | [ ] |
|
||||||
|
| 1.4 | It should show "Cleared" status as a primary-colored pill | UI | [ ] |
|
||||||
|
| 1.5 | It should show "Pending" status as a secondary-colored pill | UI | [ ] |
|
||||||
|
| 1.6 | It should show "Voided" status as a red-colored pill | UI | [ ] |
|
||||||
|
| 1.7 | It should render check numbers as links to S3 PDFs when an s3-url exists | UI | [ ] |
|
||||||
|
| 1.8 | It should show plain check number text for payments without an s3-url | UI | [ ] |
|
||||||
|
| 1.9 | It should display a links dropdown showing associated invoices and transactions | UI | [ ] |
|
||||||
|
| 1.10 | It should display checkboxes for bulk selection on each row | UI | [ ] |
|
||||||
|
| 1.11 | It should show no check number for non-check payment types | UI | [ ] |
|
||||||
|
|
||||||
**Dialog**
|
### Filtering Behaviors
|
||||||
- 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**
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Clicking "Void payments" in modal executes the bulk void
|
|---|----------|---------------|--------|
|
||||||
- Modal closes and notification shows: "Successfully voided X of Y payments."
|
| 2.1 | It should filter payments by vendor typeahead selection | Integration | [ ] |
|
||||||
|
| 2.2 | It should filter payments by date range | Integration | [ ] |
|
||||||
|
| 2.3 | It should filter payments by check number (exact match or partial text) | Integration | [ ] |
|
||||||
|
| 2.4 | It should filter payments by invoice number (exact match) | Integration | [ ] |
|
||||||
|
| 2.5 | It should filter payments by amount range (min/max) | Integration | [ ] |
|
||||||
|
| 2.6 | It should filter payments by payment type via radio cards (All, Cash, Check, Debit) | Integration | [ ] |
|
||||||
|
| 2.7 | It should support exact-match navigation to a specific payment by ID, bypassing other filters | Integration | [ ] |
|
||||||
|
| 2.8 | It should filter payments by status via route (`/payments/pending`, `/payments/cleared`, `/payments/voided`) | Integration | [ ] |
|
||||||
|
| 2.9 | It should apply all filters via HTMX with debounced triggers | Integration | [ ] |
|
||||||
|
| 2.10 | It should combine all filters with AND logic | Integration | [ ] |
|
||||||
|
| 2.11 | It should use efficient time-bounded queries for date range filtering | Integration | [ ] |
|
||||||
|
| 2.12 | It should parse check number search as Long when possible, falling back to exact string match | Unit + Integration | [ ] |
|
||||||
|
| 2.13 | Given multiple filters are applied, when the user changes one filter, then the table should refresh with the combined filter set | Integration | [ ] |
|
||||||
|
| 2.14 | It should bypass all other filters when exact-match ID is provided | Integration | [ ] |
|
||||||
|
|
||||||
|
### Sorting Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 3.1 | It should sort by client name ascending/descending | Integration | [ ] |
|
||||||
|
| 3.2 | It should sort by vendor name ascending/descending | Integration | [ ] |
|
||||||
|
| 3.3 | It should sort by bank account ascending/descending | Integration | [ ] |
|
||||||
|
| 3.4 | It should sort by check number ascending/descending | Integration | [ ] |
|
||||||
|
| 3.5 | It should sort by date ascending/descending | Integration | [ ] |
|
||||||
|
| 3.6 | It should sort by amount ascending/descending | Integration | [ ] |
|
||||||
|
| 3.7 | It should sort by status ascending/descending | Integration | [ ] |
|
||||||
|
| 3.8 | Given the user clicks a column header twice, then the sort direction should toggle | Integration | [ ] |
|
||||||
|
|
||||||
|
### Pagination Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 4.1 | It should display 25 payments per page by default | Integration | [ ] |
|
||||||
|
| 4.2 | It should allow changing the per-page count | Integration | [ ] |
|
||||||
|
| 4.3 | It should calculate the total visible float and total float across all matching payments, not just the current page | Unit | [ ] |
|
||||||
|
|
||||||
|
### Selection Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 5.1 | It should allow selecting individual payments via checkboxes | UI | [ ] |
|
||||||
|
| 5.2 | It should allow selecting all visible payments via a header checkbox | UI | [ ] |
|
||||||
|
| 5.3 | It should allow selecting all filtered payments (up to 250) for bulk operations | Integration | [ ] |
|
||||||
|
| 5.4 | Given payments are selected, when the user applies a filter, then the selection should be cleared | Integration | [ ] |
|
||||||
|
|
||||||
|
### Row Action Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 6.1 | It should show a trash icon on each row unless the payment status is already voided | UI | [ ] |
|
||||||
|
| 6.2 | It should prompt for confirmation when clicking the trash icon ("Are you sure you want to void this payment?") | UI | [ ] |
|
||||||
|
| 6.3 | Given confirmation, when voiding a payment, then the row should be removed from the table with animation | UI | [ ] |
|
||||||
|
| 6.4 | It should block voiding cleared check payments | Integration | [ ] |
|
||||||
|
|
||||||
|
### Float Display Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 7.1 | It should display a "Visible in float" pill showing the sum of pending payment amounts in the current filter view | Unit | [ ] |
|
||||||
|
| 7.2 | It should display a "Total in float" pill showing the sum of all pending payments for the selected client(s) | Unit | [ ] |
|
||||||
|
| 7.3 | It should exclude voided payments from float calculations | Unit | [ ] |
|
||||||
|
| 7.4 | It should include only pending status payments in float calculations | Unit | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bulk Void
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 8.1 | It should show a confirmation modal with warning icon and count of payments to be voided | UI | [ ] |
|
||||||
|
| 8.2 | It should support "Selected only" mode to void only checkboxed payments | UI | [ ] |
|
||||||
|
| 8.3 | It should support "All selected" mode to void all payments matching current filters (up to 250) | Integration | [ ] |
|
||||||
|
| 8.4 | It should require admin permission for bulk void operations | Integration | [ ] |
|
||||||
|
| 8.5 | Given confirmation, when voiding, then the modal should close and a notification should show "Successfully voided X of Y payments" | Integration | [ ] |
|
||||||
|
| 8.6 | It should skip payments that already have transactions and skip already-voided payments | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Check Printing
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 9.1 | It should generate physical check PDFs with MICR encoding at the bottom | Integration | [ ] |
|
||||||
|
| 9.2 | It should include payee, amount in numbers and words, date, memo, bank info, and client signature image | Integration | [ ] |
|
||||||
|
| 9.3 | It should generate voucher copies with full invoice details below the check | Integration | [ ] |
|
||||||
|
| 9.4 | It should store check PDFs in S3 under `checks/{uuid}.pdf` | Integration | [ ] |
|
||||||
|
| 9.5 | It should assign check numbers sequentially from the bank account's check number | Integration | [ ] |
|
||||||
|
| 9.6 | It should increment the bank account's check number by the number of vendors paid | Integration | [ ] |
|
||||||
|
| 9.7 | It should validate that the bank account has a starting check number | Integration | [ ] |
|
||||||
|
| 9.8 | It should merge multiple checks into a single PDF at `merged-checks/{uuid}.pdf` | Integration | [ ] |
|
||||||
|
| 9.9 | It should group invoices by vendor, creating one check per vendor per batch | Integration | [ ] |
|
||||||
|
| 9.10 | It should validate that all invoices belong to the same client and the selected bank account belongs to the same client | Integration | [ ] |
|
||||||
|
| 9.11 | It should reject check creation if the total amount is <= $0.00 | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ACH Payments
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 10.1 | It should create pending payments with `payment-type/debit` | Integration | [ ] |
|
||||||
|
| 10.2 | It should not generate check PDFs for ACH payments | Integration | [ ] |
|
||||||
|
| 10.3 | It should not create transactions for ACH payments | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Balance Credit Payments
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 11.1 | It should allow paying invoices from existing vendor credit with `payment-type/balance-credit` | Integration | [ ] |
|
||||||
|
| 11.2 | It should block balance credit payments when multiple vendors are selected | Integration | [ ] |
|
||||||
|
| 11.3 | It should offset positive-balance invoices against negative-balance invoices | Integration | [ ] |
|
||||||
|
| 11.4 | It should create a single cleared payment for the net amount, consuming credit invoices first-in | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cash Payments
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 12.1 | It should create payments with `payment-type/cash` automatically marked as cleared | Integration | [ ] |
|
||||||
|
| 12.2 | It should create an associated transaction with POSTED status | Integration | [ ] |
|
||||||
|
| 12.3 | It should use the account with numeric code 21000 for cash payment transactions | Integration | [ ] |
|
||||||
|
| 12.4 | It should set the payment date to the latest invoice date | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Cross-Cutting Behaviors
|
## Cross-Cutting Behaviors
|
||||||
|
|
||||||
### Filtering, Sorting, Pagination
|
### Voiding Behaviors
|
||||||
|
|
||||||
**Filters (all apply via HTMX with debounced triggers)**
|
| # | Behavior | Test Strategy | Status |
|
||||||
- **Vendor**: Typeahead search by vendor name
|
|---|----------|---------------|--------|
|
||||||
- **Date Range**: Start and end dates
|
| 13.1 | It should allow voiding pending payments | Integration | [ ] |
|
||||||
- **Check #**: Exact match or partial text search (parsed as Long if possible)
|
| 13.2 | It should allow voiding cash, debit, and balance-credit payments even when cleared | Integration | [ ] |
|
||||||
- **Invoice #**: Exact match on invoice number
|
| 13.3 | It should block voiding cleared check payments | Integration | [ ] |
|
||||||
- **Amount Range**: Greater-than-or-equal and less-than-or-equal inputs
|
| 13.4 | It should set the payment amount to 0.0 when voided | Integration | [ ] |
|
||||||
- **Payment Type**: Radio cards — All, Cash, Check, Debit
|
| 13.5 | It should set the payment status to voided | Integration | [ ] |
|
||||||
- **Exact Match ID**: Filter to a specific payment by Datomic entity ID
|
| 13.6 | It should remove all invoice-payment links when voiding | Integration | [ ] |
|
||||||
|
| 13.7 | It should restore invoice outstanding balances by adding back the invoice-payment amount | Integration | [ ] |
|
||||||
|
| 13.8 | It should revert invoice status to unpaid when restored balance becomes non-zero | Integration | [ ] |
|
||||||
|
| 13.9 | It should unlink associated transactions when voiding | Integration | [ ] |
|
||||||
|
|
||||||
**Sortable Columns**
|
### Permission Behaviors
|
||||||
- Client, Vendor, Bank account, Check number, Date, Amount, Status
|
|
||||||
|
|
||||||
**Pagination**
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Default 25 per page
|
|---|----------|---------------|--------|
|
||||||
- Standard start/per-page query parameters
|
| 14.1 | It should require client visibility for viewing payments | Integration | [ ] |
|
||||||
|
| 14.2 | It should require client visibility for voiding individual payments | Integration | [ ] |
|
||||||
|
| 14.3 | It should require admin permission for bulk voiding payments | Integration | [ ] |
|
||||||
|
| 14.4 | It should allow viewing S3 check PDFs to all users who can see the payment | Integration | [ ] |
|
||||||
|
|
||||||
**Route-Based Status Filtering**
|
### Lock Date Behaviors
|
||||||
- `/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
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 15.1 | It should block voiding payments dated before the client's locked-until date | Integration | [ ] |
|
||||||
|
| 15.2 | It should check lock dates on individual void operations | Integration | [ ] |
|
||||||
|
| 15.3 | It should check lock dates on bulk void operations | Integration | [ ] |
|
||||||
|
| 15.4 | It should exclude locked payments from bulk void results | Integration | [ ] |
|
||||||
|
| 15.5 | It should show a warning when some selected payments are locked | UI | [ ] |
|
||||||
|
|
||||||
**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
|
## Test Data Requirements
|
||||||
|
|
||||||
**Basic Payment**
|
| Entity | Requirements |
|
||||||
```clojure
|
|--------|-------------|
|
||||||
{:db/id "check-id"
|
| **Clients** | Multiple clients with different locations; some with locked-until dates |
|
||||||
:payment/check-number 1000
|
| **Vendors** | With/without default accounts; with/without terms and autopay settings |
|
||||||
:payment/bank-account "bank-id"
|
| **Bank Accounts** | Check, cash, and credit types; with/without starting check numbers |
|
||||||
:payment/client "client-id"
|
| **Invoices** | Various statuses, dates, amounts; positive and negative balances |
|
||||||
:payment/type :payment-type/check
|
| **Payments** | Check, cash, debit, and balance-credit types; pending, cleared, and voided statuses |
|
||||||
:payment/amount 123.50
|
| **Transactions** | Linked to cash payments with POSTED status |
|
||||||
:payment/paid-to "Someone"
|
| **Files** | Check PDFs stored in S3 |
|
||||||
:payment/status :payment-status/pending
|
|
||||||
:payment/date #inst "2022-01-01"}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Required Entities for Check Printing**
|
## Existing Tests to Preserve
|
||||||
- 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**
|
- `test/clj/auto_ap/integration/graphql/checks.clj` — Check/payment GraphQL operations
|
||||||
- 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
|
## Dependencies
|
||||||
|
|
||||||
|
|||||||
@@ -4,244 +4,402 @@
|
|||||||
|
|
||||||
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.
|
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
|
**Testing Philosophy**
|
||||||
|
- Prefer unit tests for pure business logic (calculations, validations, transformations)
|
||||||
|
- Use integration tests for database interactions and cross-system flows
|
||||||
|
- Use UI tests only for end-to-end happy paths that touch multiple pages
|
||||||
|
- Every behavior must be user-visible; no tests for implementation details
|
||||||
|
|
||||||
| 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.
|
## Testing Patterns
|
||||||
|
|
||||||
## Behaviors by Page
|
### Pattern: Grid Page Behaviors
|
||||||
|
Most list pages in Integreat follow the same pattern:
|
||||||
|
1. Fetch IDs via Datomic query with filters
|
||||||
|
2. Hydrate results via `pull-many`
|
||||||
|
3. Render table with sortable columns
|
||||||
|
4. Support selection (individual / all / all-filtered)
|
||||||
|
5. Action buttons appear conditionally based on permissions and selection state
|
||||||
|
|
||||||
### Sales Orders
|
**Test implications:** Validate the query generation, not the rendering. UI tests only need to verify one filter and one sort work end-to-end.
|
||||||
|
|
||||||
**Purpose**: Display individual sales orders with line items, charges, and payment breakdowns.
|
### Pattern: Wizard Behaviors
|
||||||
|
Wizards are multi-step forms with HTMX-driven navigation:
|
||||||
|
1. Each step is a GET that renders a form fragment
|
||||||
|
2. Form submissions are POST/PUT with validation
|
||||||
|
3. Navigation between steps updates the wizard state
|
||||||
|
4. Final submit creates/updates the entity
|
||||||
|
|
||||||
**Filters**:
|
**Test implications:** Unit test validation logic and state transitions. Integration test the full wizard flow once. UI test only the happy path.
|
||||||
- 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**:
|
### Pattern: Permission Gates
|
||||||
- Client (hidden when viewing a single client)
|
Every mutating operation checks:
|
||||||
- Date
|
1. `assert-can-see-client` — user has access to the client
|
||||||
- Source (rendered as a pill badge)
|
2. `assert-not-locked` — invoice date >= client locked-until
|
||||||
- Total
|
3. `can?` — user has the specific permission for the activity
|
||||||
- Tax
|
|
||||||
- Tip
|
|
||||||
- Payment Methods (pills for each unique charge type: cash, card, gift card, other)
|
|
||||||
|
|
||||||
**Sortable by**: client, date, total, tax, tip, source, processor
|
**Test implications:** Integration test each gate independently. UI tests only verify the happy path with a permitted user.
|
||||||
|
|
||||||
**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.
|
## Sales Orders
|
||||||
|
|
||||||
**Special Behaviors**:
|
### Display 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
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 1.1 | It should display a table with columns: Client, Date, Source, Total, Tax, Tip, Payment Methods | UI | [ ] |
|
||||||
|
| 1.2 | It should show the Client column only when multiple clients are in scope | Integration | [ ] |
|
||||||
|
| 1.3 | It should render the Source column as a pill badge | UI | [ ] |
|
||||||
|
| 1.4 | It should render each unique payment method as a pill in the Payment Methods column (cash, card, gift card, other) | UI | [ ] |
|
||||||
|
| 1.5 | It should display action buttons above the grid showing Total $ and Tax $ pills summarizing the currently filtered result set | UI | [ ] |
|
||||||
|
| 1.6 | It should show an external link icon row button when the sales order has a reference link | UI | [ ] |
|
||||||
|
|
||||||
**Purpose**: Display expected deposit records from payment processors, with links to transactions.
|
### Filtering Behaviors
|
||||||
|
|
||||||
**Filters**:
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Date range
|
|---|----------|---------------|--------|
|
||||||
- Exact match ID (shows a removable pill when active)
|
| 2.1 | It should filter sales orders by date range (start date / end date) | Integration | [ ] |
|
||||||
|
| 2.2 | It should filter sales orders by total amount range (min / max) | Integration | [ ] |
|
||||||
|
| 2.3 | It should filter sales orders by payment method via radio cards: All, Cash, Card, Gift Card, Other | Integration | [ ] |
|
||||||
|
| 2.4 | It should filter sales orders by processor via radio cards: All, Square, Doordash, Uber Eats, Grubhub, Koala, EZCater, No Processor | Integration | [ ] |
|
||||||
|
| 2.5 | It should filter sales orders by category text input matching order line item category | Integration | [ ] |
|
||||||
|
| 2.6 | Given multiple filters are applied, when the user changes one filter, then the table should refresh with the combined filter set | Integration | [ ] |
|
||||||
|
|
||||||
**Columns**:
|
### Sorting Behaviors
|
||||||
- Client (hidden when viewing a single client)
|
|
||||||
- Date
|
|
||||||
- Sales Date
|
|
||||||
- Total
|
|
||||||
- Fee
|
|
||||||
|
|
||||||
**Sortable by**: client, location, date, total, fee
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 3.1 | It should sort by client name ascending/descending | Integration | [ ] |
|
||||||
|
| 3.2 | It should sort by date ascending/descending | Integration | [ ] |
|
||||||
|
| 3.3 | It should sort by total amount ascending/descending | Integration | [ ] |
|
||||||
|
| 3.4 | It should sort by tax amount ascending/descending | Integration | [ ] |
|
||||||
|
| 3.5 | It should sort by tip amount ascending/descending | Integration | [ ] |
|
||||||
|
| 3.6 | It should sort by source ascending/descending | Integration | [ ] |
|
||||||
|
| 3.7 | It should sort by processor ascending/descending | Integration | [ ] |
|
||||||
|
| 3.8 | Given the user clicks a column header twice, then the sort direction should toggle | Integration | [ ] |
|
||||||
|
|
||||||
**Row Buttons**:
|
### Pagination Behaviors
|
||||||
- External link icon when `:expected-deposit/reference-link` exists
|
|
||||||
- "Transaction" button linking to the associated transaction (if `transaction/_expected-deposit` exists)
|
|
||||||
|
|
||||||
**Special Behaviors**:
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Hydration computes a `:totals` breakdown per expected deposit, aggregating charges by sales date with count and amount.
|
|---|----------|---------------|--------|
|
||||||
|
| 4.1 | It should display 25 sales orders per page by default | Integration | [ ] |
|
||||||
|
| 4.2 | It should allow changing the per-page count | Integration | [ ] |
|
||||||
|
| 4.3 | It should calculate the total amount and tax across ALL matching sales orders, not just the current page | Unit | [ ] |
|
||||||
|
|
||||||
### Tenders
|
---
|
||||||
|
|
||||||
**Purpose**: Display individual charge/tender records (payments).
|
## Expected Deposits
|
||||||
|
|
||||||
**Filters**:
|
### Display Behaviors
|
||||||
- Date range
|
|
||||||
- Processor (same radio options as Sales Orders)
|
|
||||||
- Total amount range (min / max)
|
|
||||||
|
|
||||||
**Columns**:
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Client (hidden when viewing a single client)
|
|---|----------|---------------|--------|
|
||||||
- Date
|
| 5.1 | It should display a table with columns: Client, Date, Sales Date, Total, Fee | UI | [ ] |
|
||||||
- Total
|
| 5.2 | It should show the Client column only when multiple clients are in scope | Integration | [ ] |
|
||||||
- Processor (rendered as a pill badge)
|
| 5.3 | It should show a totals breakdown per expected deposit, aggregating charges by sales date with count and amount | UI | [ ] |
|
||||||
- Tip
|
| 5.4 | It should show an external link icon row button when the expected deposit has a reference link | UI | [ ] |
|
||||||
- Links
|
| 5.5 | It should show a "Transaction" button linking to the associated transaction when one exists | UI | [ ] |
|
||||||
|
|
||||||
**Sortable by**: client, date, total, tip, processor
|
### Filtering Behaviors
|
||||||
|
|
||||||
**Row Buttons**: External link icon when `:charge/reference-link` exists.
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 6.1 | It should filter expected deposits by date range | Integration | [ ] |
|
||||||
|
| 6.2 | It should support exact match ID to jump to a specific record, showing a removable pill when active | Integration | [ ] |
|
||||||
|
| 6.3 | Given multiple filters are applied, when the user changes one filter, then the table should refresh with the combined filter set | Integration | [ ] |
|
||||||
|
|
||||||
**Special Behaviors**:
|
### Sorting Behaviors
|
||||||
- Links column shows an "expected deposit" pill linking to the associated expected deposit if one exists.
|
|
||||||
|
|
||||||
### Refunds
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 7.1 | It should sort by client name ascending/descending | Integration | [ ] |
|
||||||
|
| 7.2 | It should sort by location ascending/descending | Integration | [ ] |
|
||||||
|
| 7.3 | It should sort by date ascending/descending | Integration | [ ] |
|
||||||
|
| 7.4 | It should sort by total amount ascending/descending | Integration | [ ] |
|
||||||
|
| 7.5 | It should sort by fee amount ascending/descending | Integration | [ ] |
|
||||||
|
| 7.6 | Given the user clicks a column header twice, then the sort direction should toggle | Integration | [ ] |
|
||||||
|
|
||||||
**Purpose**: Display refund records.
|
### Pagination Behaviors
|
||||||
|
|
||||||
**Filters**:
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Date range
|
|---|----------|---------------|--------|
|
||||||
- Total amount range (min / max)
|
| 8.1 | It should display 25 expected deposits per page by default | Integration | [ ] |
|
||||||
|
| 8.2 | It should allow changing the per-page count | Integration | [ ] |
|
||||||
|
|
||||||
**Columns**:
|
---
|
||||||
- Client (hidden when viewing a single client)
|
|
||||||
- Date
|
|
||||||
- Total
|
|
||||||
- Type
|
|
||||||
- Fee
|
|
||||||
|
|
||||||
**Sortable by**: client, date, total, fee, type
|
## Tenders
|
||||||
|
|
||||||
**Row Buttons**: None.
|
### Display Behaviors
|
||||||
|
|
||||||
### Cash Drawer Shifts
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 9.1 | It should display a table with columns: Client, Date, Total, Processor, Tip, Links | UI | [ ] |
|
||||||
|
| 9.2 | It should show the Client column only when multiple clients are in scope | Integration | [ ] |
|
||||||
|
| 9.3 | It should render the Processor column as a pill badge | UI | [ ] |
|
||||||
|
| 9.4 | It should show an external link icon row button when the tender has a reference link | UI | [ ] |
|
||||||
|
| 9.5 | It should show an "expected deposit" pill in the Links column when an associated expected deposit exists | UI | [ ] |
|
||||||
|
|
||||||
**Purpose**: Display cash drawer shift records.
|
### Filtering Behaviors
|
||||||
|
|
||||||
**Filters**:
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Date range
|
|---|----------|---------------|--------|
|
||||||
- Total amount range (min / max)
|
| 10.1 | It should filter tenders by date range | Integration | [ ] |
|
||||||
|
| 10.2 | It should filter tenders by processor via radio cards: All, Square, Doordash, Uber Eats, Grubhub, Koala, EZCater, No Processor | Integration | [ ] |
|
||||||
|
| 10.3 | It should filter tenders by total amount range (min / max) | Integration | [ ] |
|
||||||
|
| 10.4 | Given multiple filters are applied, when the user changes one filter, then the table should refresh with the combined filter set | Integration | [ ] |
|
||||||
|
|
||||||
**Columns**:
|
### Sorting Behaviors
|
||||||
- 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
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 11.1 | It should sort by client name ascending/descending | Integration | [ ] |
|
||||||
|
| 11.2 | It should sort by date ascending/descending | Integration | [ ] |
|
||||||
|
| 11.3 | It should sort by total amount ascending/descending | Integration | [ ] |
|
||||||
|
| 11.4 | It should sort by tip amount ascending/descending | Integration | [ ] |
|
||||||
|
| 11.5 | It should sort by processor ascending/descending | Integration | [ ] |
|
||||||
|
| 11.6 | Given the user clicks a column header twice, then the sort direction should toggle | Integration | [ ] |
|
||||||
|
|
||||||
**Row Buttons**: None.
|
### Pagination Behaviors
|
||||||
|
|
||||||
### Sales Summaries
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 12.1 | It should display 25 tenders per page by default | Integration | [ ] |
|
||||||
|
| 12.2 | It should allow changing the per-page count | Integration | [ ] |
|
||||||
|
|
||||||
**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.
|
## Refunds
|
||||||
|
|
||||||
**Filters**:
|
### Display Behaviors
|
||||||
- Date range (currently commented out in UI but schema supports it)
|
|
||||||
|
|
||||||
**Columns**:
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Client (hidden when viewing a single client)
|
|---|----------|---------------|--------|
|
||||||
- Date
|
| 13.1 | It should display a table with columns: Client, Date, Total, Type, Fee | UI | [ ] |
|
||||||
- Debits (list of debit-line items with category, amount, and "missing account" warning pill if unmapped)
|
| 13.2 | It should show the Client column only when multiple clients are in scope | Integration | [ ] |
|
||||||
- Credits (list of credit-line items with category, amount, and "missing account" warning pill if unmapped)
|
|
||||||
|
|
||||||
**Sortable by**: client, date, debits, credits
|
### Filtering Behaviors
|
||||||
|
|
||||||
**Row Buttons**: Edit (pencil) icon opens the edit wizard modal.
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 14.1 | It should filter refunds by date range | Integration | [ ] |
|
||||||
|
| 14.2 | It should filter refunds by total amount range (min / max) | Integration | [ ] |
|
||||||
|
| 14.3 | Given multiple filters are applied, when the user changes one filter, then the table should refresh with the combined filter set | Integration | [ ] |
|
||||||
|
|
||||||
**Edit Wizard Behaviors**:
|
### Sorting Behaviors
|
||||||
- Modal dialog titled "New invoice" (legacy title).
|
|
||||||
- Displays a data grid of summary items with columns: Category, Account, Debits, Credits.
|
| # | Behavior | Test Strategy | Status |
|
||||||
- **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.
|
| 15.1 | It should sort by client name ascending/descending | Integration | [ ] |
|
||||||
- New manual items can be added via "New Summary Item" row.
|
| 15.2 | It should sort by date ascending/descending | Integration | [ ] |
|
||||||
- Manual items can be removed via X button.
|
| 15.3 | It should sort by total amount ascending/descending | Integration | [ ] |
|
||||||
- Validation: an item cannot have both credit and debit amounts.
|
| 15.4 | It should sort by fee amount ascending/descending | Integration | [ ] |
|
||||||
- **Total row**: shows running totals for debits and credits.
|
| 15.5 | It should sort by type ascending/descending | Integration | [ ] |
|
||||||
- **Unbalanced row**: shows the difference when debits ≠ credits.
|
| 15.6 | Given the user clicks a column header twice, then the sort direction should toggle | Integration | [ ] |
|
||||||
- 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.
|
### Pagination Behaviors
|
||||||
- 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.
|
| # | Behavior | Test Strategy | Status |
|
||||||
- After save, the row flashes and the modal closes.
|
|---|----------|---------------|--------|
|
||||||
|
| 16.1 | It should display 25 refunds per page by default | Integration | [ ] |
|
||||||
|
| 16.2 | It should allow changing the per-page count | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cash Drawer Shifts
|
||||||
|
|
||||||
|
### Display Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 17.1 | It should display a table with columns: Client, Date, Paid in, Paid out, Expected cash, Opened cash | UI | [ ] |
|
||||||
|
| 17.2 | It should show the Client column only when multiple clients are in scope | Integration | [ ] |
|
||||||
|
|
||||||
|
### Filtering Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 18.1 | It should filter cash drawer shifts by date range | Integration | [ ] |
|
||||||
|
| 18.2 | It should filter cash drawer shifts by total amount range (min / max) | Integration | [ ] |
|
||||||
|
| 18.3 | Given multiple filters are applied, when the user changes one filter, then the table should refresh with the combined filter set | Integration | [ ] |
|
||||||
|
|
||||||
|
### Sorting Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 19.1 | It should sort by client name ascending/descending | Integration | [ ] |
|
||||||
|
| 19.2 | It should sort by date ascending/descending | Integration | [ ] |
|
||||||
|
| 19.3 | It should sort by paid-in amount ascending/descending | Integration | [ ] |
|
||||||
|
| 19.4 | It should sort by paid-out amount ascending/descending | Integration | [ ] |
|
||||||
|
| 19.5 | It should sort by expected-cash amount ascending/descending | Integration | [ ] |
|
||||||
|
| 19.6 | It should sort by opened-cash amount ascending/descending | Integration | [ ] |
|
||||||
|
| 19.7 | Given the user clicks a column header twice, then the sort direction should toggle | Integration | [ ] |
|
||||||
|
|
||||||
|
### Pagination Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 20.1 | It should display 25 cash drawer shifts per page by default | Integration | [ ] |
|
||||||
|
| 20.2 | It should allow changing the per-page count | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sales Summaries
|
||||||
|
|
||||||
|
### Display Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 21.1 | It should display a table with columns: Client, Date, Debits, Credits | UI | [ ] |
|
||||||
|
| 21.2 | It should show the Client column only when multiple clients are in scope | Integration | [ ] |
|
||||||
|
| 21.3 | It should display debit-line items with category and amount | UI | [ ] |
|
||||||
|
| 21.4 | It should display credit-line items with category and amount | UI | [ ] |
|
||||||
|
| 21.5 | It should show a red "missing account" warning pill for unmapped items | UI | [ ] |
|
||||||
|
| 21.6 | It should show a green "Total" pill for balanced summaries (debits equal credits) | UI | [ ] |
|
||||||
|
| 21.7 | It should show a red "Total" pill for unbalanced summaries (debits do not equal credits) | UI | [ ] |
|
||||||
|
| 21.8 | It should show an edit (pencil) icon row button opening the edit wizard modal | UI | [ ] |
|
||||||
|
|
||||||
|
### Filtering Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 22.1 | It should filter sales summaries by date range | Integration | [ ] |
|
||||||
|
| 22.2 | Given multiple filters are applied, when the user changes one filter, then the table should refresh with the combined filter set | Integration | [ ] |
|
||||||
|
|
||||||
|
### Sorting Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 23.1 | It should sort by client name ascending/descending | Integration | [ ] |
|
||||||
|
| 23.2 | It should sort by date ascending/descending | Integration | [ ] |
|
||||||
|
| 23.3 | It should sort by debits ascending/descending | Integration | [ ] |
|
||||||
|
| 23.4 | It should sort by credits ascending/descending | Integration | [ ] |
|
||||||
|
| 23.5 | Given the user clicks a column header twice, then the sort direction should toggle | Integration | [ ] |
|
||||||
|
|
||||||
|
### Pagination Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 24.1 | It should display 25 sales summaries per page by default | Integration | [ ] |
|
||||||
|
| 24.2 | It should allow changing the per-page count | Integration | [ ] |
|
||||||
|
|
||||||
|
### Edit Wizard Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 25.1 | It should open a modal dialog when the edit button is clicked | UI | [ ] |
|
||||||
|
| 25.2 | It should display a data grid of summary items with columns: Category, Account, Debits, Credits | UI | [ ] |
|
||||||
|
| 25.3 | It should render auto items (non-manual) with read-only Category and amount, editable Account via typeahead | UI | [ ] |
|
||||||
|
| 25.4 | It should render manual items with editable Category (text input), Account (typeahead), Debit amount, and Credit amount | UI | [ ] |
|
||||||
|
| 25.5 | It should allow adding new manual items via a "New Summary Item" row | UI | [ ] |
|
||||||
|
| 25.6 | It should allow removing manual items via an X button | UI | [ ] |
|
||||||
|
| 25.7 | It should validate that an item cannot have both credit and debit amounts | Unit + Integration | [ ] |
|
||||||
|
| 25.8 | It should display a total row with running totals for debits and credits | UI | [ ] |
|
||||||
|
| 25.9 | It should display an unbalanced row showing the difference when debits do not equal credits | UI | [ ] |
|
||||||
|
| 25.10 | It should search accounts scoped to the client with purpose "invoice" in the account typeahead | Integration | [ ] |
|
||||||
|
| 25.11 | It should flash the row and close the modal after successful save | UI | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Cross-Cutting Behaviors
|
## Cross-Cutting Behaviors
|
||||||
|
|
||||||
### Filtering, Sorting, Pagination
|
### HTMX Live Filtering Behaviors
|
||||||
|
|
||||||
**HTMX Live Filtering**:
|
| # | Behavior | Test Strategy | Status |
|
||||||
- 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.
|
| 26.1 | It should trigger table refresh on filter form change with a 500ms debounce | Integration | [ ] |
|
||||||
- The table route swaps the grid contents and updates the browser URL via `hx-push-url`.
|
| 26.2 | It should trigger table refresh on hot-filter keyup with a 1000ms debounce | Integration | [ ] |
|
||||||
|
| 26.3 | It should POST to the table route and swap the grid contents | Integration | [ ] |
|
||||||
|
| 26.4 | It should update the browser URL via hx-push-url when filters change | Integration | [ ] |
|
||||||
|
|
||||||
**Date Range**:
|
### Date Range Behaviors
|
||||||
- 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**:
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Most pages support `total-gte` and `total-lte` (money inputs).
|
|---|----------|---------------|--------|
|
||||||
|
| 27.1 | It should support start-date and end-date query params on all pages | Integration | [ ] |
|
||||||
|
| 27.2 | It should render the date range filter consistently across all pages | UI | [ ] |
|
||||||
|
| 27.3 | Given a date range with no start or end date, then the query should use scan functions with nil boundaries | Integration | [ ] |
|
||||||
|
|
||||||
**Exact Match ID**:
|
### Total Range Behaviors
|
||||||
- Most pages support `exact-match-id` to jump to a specific record.
|
|
||||||
|
|
||||||
**Sorting**:
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Click a sortable column header to toggle ascending/descending.
|
|---|----------|---------------|--------|
|
||||||
- Multi-sort is supported; active sorts appear as removable pills above the grid.
|
| 28.1 | It should support total-gte and total-lte query params on pages with amount filters | Integration | [ ] |
|
||||||
- Remove a sort by clicking the X on its pill.
|
| 28.2 | It should render money inputs for the total range filter | UI | [ ] |
|
||||||
- Default sort is by date descending for most pages.
|
|
||||||
|
|
||||||
**Pagination**:
|
### Exact Match ID Behaviors
|
||||||
- Default 25 rows per page.
|
|
||||||
- Controls include first/previous/next/last and per-page selector.
|
|
||||||
- Total count is displayed above the grid.
|
|
||||||
|
|
||||||
**Client Scoping**:
|
| # | Behavior | Test Strategy | Status |
|
||||||
- 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.
|
| 29.1 | It should support exact-match-id to jump to a specific record on applicable pages | Integration | [ ] |
|
||||||
- URL may include `client-id` or `client-code` params.
|
| 29.2 | It should show a removable pill when exact-match-id is active | UI | [ ] |
|
||||||
|
|
||||||
### Permissions
|
### Sorting Behaviors
|
||||||
|
|
||||||
- POS pages require `(can? identity {:subject :sales :activity :read})`.
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Sales Summaries requires admin access (`wrap-admin`).
|
|---|----------|---------------|--------|
|
||||||
- All routes redirect unauthenticated users.
|
| 30.1 | It should toggle ascending/descending sort when a sortable column header is clicked | Integration | [ ] |
|
||||||
|
| 30.2 | It should support multi-sort with active sorts appearing as removable pills above the grid | Integration | [ ] |
|
||||||
|
| 30.3 | It should remove a sort when the X on its pill is clicked | Integration | [ ] |
|
||||||
|
| 30.4 | It should default to sort by date descending for most pages | Integration | [ ] |
|
||||||
|
|
||||||
## Edge Cases
|
### Pagination Behaviors
|
||||||
|
|
||||||
- **No results**: Grid displays "Total X: 0" with empty rows.
|
| # | Behavior | Test Strategy | Status |
|
||||||
- **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.
|
| 31.1 | It should display first/previous/next/last pagination controls | UI | [ ] |
|
||||||
- **Missing expected deposit link on Tenders**: Links cell is empty when no associated expected deposit exists.
|
| 31.2 | It should display the total count above the grid | UI | [ ] |
|
||||||
- **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.
|
### Client Scoping Behaviors
|
||||||
- **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.
|
| # | Behavior | Test Strategy | Status |
|
||||||
- **Category filter on Sales Orders**: Empty string or nil category param is treated as no filter.
|
|---|----------|---------------|--------|
|
||||||
|
| 32.1 | It should scope all queries to the user's accessible clients (trimmed to max 20) | Integration | [ ] |
|
||||||
|
| 32.2 | It should hide the Client column when only one client is in scope | Integration | [ ] |
|
||||||
|
| 32.3 | It should support client-id and client-code URL params | Integration | [ ] |
|
||||||
|
|
||||||
|
### Permission Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 33.1 | It should require `(can? identity {:subject :sales :activity :read})` to access POS pages | Integration | [ ] |
|
||||||
|
| 33.2 | It should require admin access (`wrap-admin`) to access Sales Summaries | Integration | [ ] |
|
||||||
|
| 33.3 | It should redirect unauthenticated users | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Test Data Requirements
|
## Test Data Requirements
|
||||||
|
|
||||||
- Clients with `db/id`, `client/name`, `client/code`.
|
| Entity | Requirements |
|
||||||
- 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`.
|
| **Clients** | Multiple clients with `db/id`, `client/name`, `client/code`; some with locked-until dates |
|
||||||
- Tenders (charges) with: `:charge/date`, `:charge/total`, `:charge/tip`, `:charge/processor`, optional `expected-deposit/_charges`.
|
| **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`) |
|
||||||
- Refunds with: `:sales-refund/date`, `:sales-refund/total`, `:sales-refund/fee`, `:sales-refund/type`.
|
| **Expected Deposits** | With `:expected-deposit/date`, `:expected-deposit/total`, `:expected-deposit/fee`, `:expected-deposit/client`, optional `transaction/_expected-deposit` |
|
||||||
- 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`.
|
| **Tenders (Charges)** | With `:charge/date`, `:charge/total`, `:charge/tip`, `:charge/processor`, optional `expected-deposit/_charges` |
|
||||||
- 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`).
|
| **Refunds** | With `:sales-refund/date`, `:sales-refund/total`, `:sales-refund/fee`, `:sales-refund/type` |
|
||||||
|
| **Cash Drawer Shifts** | With `:cash-drawer-shift/date`, `:cash-drawer-shift/paid-in`, `:cash-drawer-shift/paid-out`, `:cash-drawer-shift/expected-cash`, `:cash-drawer-shift/opened-cash` |
|
||||||
|
| **Sales Summaries** | With `:sales-summary/date`, `:sales-summary/client`, `:sales-summary/items` (with `ledger-mapped/ledger-side`, `ledger-mapped/amount`, `sales-summary-item/category`, optional `ledger-mapped/account`) |
|
||||||
|
| **Accounts** | With purpose "invoice", scoped to clients, for Sales Summaries edit wizard |
|
||||||
|
|
||||||
|
## Existing Tests to Preserve
|
||||||
|
|
||||||
|
- `test/clj/auto_ap/ssr/pos/sales_orders_test.clj` — Sales orders grid behaviors
|
||||||
|
- `test/clj/auto_ap/ssr/pos/expected_deposits_test.clj` — Expected deposits grid behaviors
|
||||||
|
- `test/clj/auto_ap/ssr/pos/tenders_test.clj` — Tenders grid behaviors
|
||||||
|
- `test/clj/auto_ap/ssr/pos/refunds_test.clj` — Refunds grid behaviors
|
||||||
|
- `test/clj/auto_ap/ssr/pos/cash_drawer_shifts_test.clj` — Cash drawer shifts grid behaviors
|
||||||
|
- `test/clj/auto_ap/ssr/admin/sales_summaries_test.clj` — Sales summaries admin behaviors
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
- **Grid system**: `auto-ap.ssr.grid-page-helper` provides `build`, `page-route`, `table-route`, `row*`, `table*`.
|
- Datomic (primary store)
|
||||||
- **Components**: `auto-ap.ssr.components` for data grids, pills, buttons, inputs.
|
- HTMX/Alpine.js (frontend interactivity)
|
||||||
- **Querying**: `auto-ap.datomic` for `query2`, `pull-many`, `apply-pagination`, `apply-sort-3`.
|
- Grid system: `auto-ap.ssr.grid-page-helper` provides `build`, `page-route`, `table-route`, `row*`, `table*`
|
||||||
- **Ions**: `iol-ion.query/scan-sales-orders`, `scan-expected-deposits`, `scan-charges`, `scan-sales-refunds`, `scan-cash-drawer-shifts`.
|
- Components: `auto-ap.ssr.components` for data grids, pills, buttons, inputs
|
||||||
- **Permissions**: `auto-ap.permissions/can?`.
|
- Querying: `auto-ap.datomic` for `query2`, `pull-many`, `apply-pagination`, `apply-sort-3`
|
||||||
- **Time**: `auto-ap.time` for date formatting and localization.
|
- Ions: `iol-ion.query/scan-sales-orders`, `scan-expected-deposits`, `scan-charges`, `scan-sales-refunds`, `scan-cash-drawer-shifts`
|
||||||
- **Schema validation**: Malli schemas enforce query params on every request.
|
- Permissions: `auto-ap.permissions/can?`
|
||||||
|
- Time: `auto-ap.time` for date formatting and localization
|
||||||
|
- Schema validation: Malli schemas enforce query params on every request
|
||||||
|
|||||||
@@ -4,144 +4,163 @@
|
|||||||
|
|
||||||
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.
|
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
|
**Testing Philosophy**
|
||||||
|
- Prefer unit tests for pure business logic (query parsing, date formatting, number formatting)
|
||||||
|
- Use integration tests for database interactions and cross-system flows
|
||||||
|
- Use UI tests only for end-to-end happy paths that touch multiple pages
|
||||||
|
- Every behavior must be user-visible; no tests for implementation details
|
||||||
|
|
||||||
| Route | Method | Handler | 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
|
## Testing Patterns
|
||||||
|
|
||||||
### Unit Tests
|
### Pattern: Search Query Parsing
|
||||||
|
Search queries are transformed before being sent to Solr:
|
||||||
|
1. Bare words become text search clauses joined with `AND`
|
||||||
|
2. Quoted phrases are preserved as exact tokens
|
||||||
|
3. Type keywords (`invoice`, `payment`, `transaction`, `journal-entry`) filter by document type
|
||||||
|
4. Dates in normal or ISO format are converted to Solr date format
|
||||||
|
5. Decimal numbers are formatted to 2 decimal places
|
||||||
|
6. Unparseable tokens pass through unchanged
|
||||||
|
|
||||||
- `q->solr-q` transforms user query into Solr query string
|
**Test implications:** Unit test each transformation rule. Integration test the full query pipeline once.
|
||||||
- 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
|
### Pattern: Permission Filtering
|
||||||
|
All search results are filtered by the user's client access permissions.
|
||||||
|
|
||||||
- `GET /search` with authenticated user returns 200 with search modal HTML
|
**Test implications:** Integration test with users having different client access levels.
|
||||||
- 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
|
## Search
|
||||||
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
|
### Modal Behaviors
|
||||||
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
|
| # | Behavior | Test Strategy | Status |
|
||||||
1. User opens search modal
|
|---|----------|---------------|--------|
|
||||||
2. User types "5/5/2034"
|
| 1.1 | It should open a search modal when the user clicks the search icon in the navbar | UI | [ ] |
|
||||||
3. Date is parsed and converted to Solr format
|
| 1.2 | It should display an autofocused search input with placeholder text in the modal | UI | [ ] |
|
||||||
4. Results matching that date appear
|
| 1.3 | It should trigger a search after 300ms debounce when the user types in the search input | Integration | [ ] |
|
||||||
|
| 1.4 | It should display the search modal without results when no query is provided | Integration | [ ] |
|
||||||
|
|
||||||
#### Empty Search Results
|
### Query Parsing Behaviors
|
||||||
1. User opens search modal
|
|
||||||
2. User types a query with no matches
|
|
||||||
3. "No results found." message displays
|
|
||||||
|
|
||||||
#### Days-Ago Indicator
|
| # | Behavior | Test Strategy | Status |
|
||||||
1. User views a page containing a `days-ago` HTMX element
|
|---|----------|---------------|--------|
|
||||||
2. Element fetches `/days-ago?date=<date>`
|
| 2.1 | It should convert bare words in the search query to text search clauses joined with `AND` | Unit | [ ] |
|
||||||
3. Colored pill renders showing relative time (e.g., "45 days ago" in secondary color)
|
| 2.2 | It should preserve quoted phrases in the search query as exact text matches | Unit | [ ] |
|
||||||
|
| 2.3 | It should filter by document type when the user includes a type keyword (`invoice`, `payment`, `transaction`, `journal-entry`) | Unit | [ ] |
|
||||||
|
| 2.4 | It should convert dates in normal format (e.g., `5/5/2034`) to Solr-compatible date format | Unit | [ ] |
|
||||||
|
| 2.5 | It should convert dates in ISO format to Solr-compatible date format | Unit | [ ] |
|
||||||
|
| 2.6 | It should pass through unparseable dates unchanged | Unit | [ ] |
|
||||||
|
| 2.7 | It should format decimal numbers to exactly 2 decimal places with `HALF_UP` rounding | Unit | [ ] |
|
||||||
|
| 2.8 | It should pass through integer numbers unchanged | Unit | [ ] |
|
||||||
|
| 2.9 | It should handle mixed type keywords and text tokens in the search query | Unit | [ ] |
|
||||||
|
| 2.10 | It should handle multiple type keywords in the search query | Unit | [ ] |
|
||||||
|
|
||||||
## Edge Cases
|
### Results Display Behaviors
|
||||||
|
|
||||||
### Search
|
| # | Behavior | Test Strategy | Status |
|
||||||
- **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
|
| 3.1 | It should display search results as cards below the search input | UI | [ ] |
|
||||||
- **Very long query**: Passes through to Solr; UI handles long text via flex layout
|
| 3.2 | It should show a type icon on each result card | UI | [ ] |
|
||||||
- **No accessible clients**: Returns empty results even if Solr has matching documents
|
| 3.3 | It should show the document type name on each result card | UI | [ ] |
|
||||||
- **Solr unavailable**: Behavior depends on `solr/impl` (MockSolrClient returns nil/empty)
|
| 3.4 | It should show the client code as a pill on each result card | UI | [ ] |
|
||||||
- **Mixed type keywords and text**: "invoice 1000" produces `type:invoice AND _text_:"1000"`
|
| 3.5 | It should show the amount as a pill on each result card | UI | [ ] |
|
||||||
- **Multiple type keywords**: "invoice payment" produces `type:invoice AND type:payment` (likely zero results)
|
| 3.6 | It should show the vendor name as a pill when present on each result card | UI | [ ] |
|
||||||
- **Numeric tokens with commas/currency**: `$1,000.50` passes through as literal text search
|
| 3.7 | It should show the date on each result card | UI | [ ] |
|
||||||
- **Future dates**: Date parsing accepts future dates; Solr query includes them
|
| 3.8 | It should show the description or number on each result card | UI | [ ] |
|
||||||
|
| 3.9 | It should link each result card to the appropriate detail page with an `exact-match-id` parameter | Integration | [ ] |
|
||||||
|
| 3.10 | It should open the detail page in a new tab when the user clicks the external link icon | UI | [ ] |
|
||||||
|
| 3.11 | It should filter results to only show documents from clients the user can access | Integration | [ ] |
|
||||||
|
|
||||||
### Indicators
|
### Empty Results Behaviors
|
||||||
- **Nil date**: Returns empty div, no error
|
|
||||||
- **Invalid date format**: Schema enforcement rejects before handler executes
|
| # | Behavior | Test Strategy | Status |
|
||||||
- **Same-day date**: Returns "0 days ago" with primary color
|
|---|----------|---------------|--------|
|
||||||
- **Very old dates**: Returns "N days ago" with red color (90+ days threshold)
|
| 4.1 | It should display "No results found." when the query matches no documents | UI | [ ] |
|
||||||
|
| 4.2 | It should return an empty results list when the user has no accessible clients | Integration | [ ] |
|
||||||
|
| 4.3 | It should return an empty results list when Solr is unavailable | Integration | [ ] |
|
||||||
|
|
||||||
|
### Type Filter Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 5.1 | It should filter results to only show payment documents when the user types "payment" | Integration | [ ] |
|
||||||
|
| 5.2 | It should filter results to only show invoice documents when the user types "invoice" | Integration | [ ] |
|
||||||
|
| 5.3 | It should show the payment icon and link to the payments detail page for payment results | UI | [ ] |
|
||||||
|
|
||||||
|
### Date Search Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 6.1 | It should return documents matching a date in normal format (e.g., `5/5/2034`) | Integration | [ ] |
|
||||||
|
| 6.2 | It should return documents matching a date in ISO format | Integration | [ ] |
|
||||||
|
| 6.3 | It should accept future dates in the search query | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Indicators
|
||||||
|
|
||||||
|
### Days-Ago Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 7.1 | It should display a relative date badge showing "N days ago" for past dates | UI | [ ] |
|
||||||
|
| 7.2 | It should display a relative date badge showing "N days from now" for future dates | UI | [ ] |
|
||||||
|
| 7.3 | It should show the days-ago badge in primary color for dates less than 30 days old | Unit + UI | [ ] |
|
||||||
|
| 7.4 | It should show the days-ago badge in secondary color for dates 30-59 days old | Unit + UI | [ ] |
|
||||||
|
| 7.5 | It should show the days-ago badge in yellow color for dates 60-89 days old | Unit + UI | [ ] |
|
||||||
|
| 7.6 | It should show the days-ago badge in red color for dates 90 or more days old | Unit + UI | [ ] |
|
||||||
|
| 7.7 | It should show "0 days ago" with primary color for same-day dates | Unit + UI | [ ] |
|
||||||
|
| 7.8 | It should render an empty indicator when the date is nil | Unit + UI | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-Cutting Behaviors
|
||||||
|
|
||||||
|
### Permission Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 8.1 | It should require authentication to access the search endpoint | Integration | [ ] |
|
||||||
|
| 8.2 | It should require authentication to access the days-ago endpoint | Integration | [ ] |
|
||||||
|
| 8.3 | It should redirect unauthenticated users to the login page when accessing search or days-ago | Integration | [ ] |
|
||||||
|
|
||||||
|
### Error Handling Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 9.1 | It should reject invalid date formats for the days-ago endpoint via schema validation | Integration | [ ] |
|
||||||
|
| 9.2 | It should handle special characters in the search query without errors | Integration | [ ] |
|
||||||
|
| 9.3 | It should handle very long search queries without UI breakage | UI | [ ] |
|
||||||
|
| 9.4 | It should handle numeric tokens with commas or currency symbols as literal text search | Unit | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Test Data Requirements
|
## Test Data Requirements
|
||||||
|
|
||||||
### Solr Index
|
| Entity | Requirements |
|
||||||
- Indexed documents of all four types: `invoice`, `payment`, `transaction`, `journal-entry`
|
|--------|-------------|
|
||||||
- Documents span multiple clients
|
| **Solr Index** | Indexed documents of all four types: `invoice`, `payment`, `transaction`, `journal-entry`; spanning multiple clients; with varying dates, amounts, descriptions, numbers, and vendor names |
|
||||||
- Documents with varying dates, amounts, descriptions, numbers, and vendor names
|
| **Users** | Authenticated user with access to subset of clients; authenticated user with access to all clients; admin user |
|
||||||
- Documents with and without vendor associations
|
| **Clients** | Multiple 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` |
|
||||||
|
|
||||||
### Users
|
## Existing Tests to Preserve
|
||||||
- Authenticated user with access to subset of clients
|
|
||||||
- Authenticated user with access to all clients
|
|
||||||
- Admin user (for full client visibility)
|
|
||||||
|
|
||||||
### Datomic Entities
|
No existing test files specified for Search & Indicators.
|
||||||
- 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
|
## Dependencies
|
||||||
|
|
||||||
### External Services
|
|
||||||
- **Solr**: Full-text search index (`auto-ap.solr`). Uses `MockSolrClient` in test environments without Solr configured
|
- **Solr**: Full-text search index (`auto-ap.solr`). Uses `MockSolrClient` in test environments without Solr configured
|
||||||
- **Datomic**: Client visibility checks pull user/client associations
|
- **Datomic**: Client visibility checks pull user/client associations
|
||||||
|
|
||||||
### Frontend Libraries
|
|
||||||
- **HTMX**: Modal loading, search debounce (`keyup changed delay:300ms`), indicator spinner
|
- **HTMX**: Modal loading, search debounce (`keyup changed delay:300ms`), indicator spinner
|
||||||
- **Alpine.js**: Modal card structure
|
- **Alpine.js**: Modal card structure
|
||||||
|
- **Middleware**: `wrap-secure` requires authentication; `wrap-client-redirect-unauthenticated` redirects unauthenticated to `/login`; `wrap-schema-enforce` validates `date` query parameter for `/days-ago`
|
||||||
### 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
|
- **Invoices**: Search results link to invoice detail pages
|
||||||
- **Payments**: Search results link to payment detail pages
|
- **Payments**: Search results link to payment detail pages
|
||||||
- **Transactions**: Search results link to transaction detail pages
|
- **Transactions**: Search results link to transaction detail pages
|
||||||
|
|||||||
@@ -2,300 +2,309 @@
|
|||||||
|
|
||||||
## Overview
|
## 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.
|
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.
|
||||||
|
|
||||||
## Routes & Pages
|
**Testing Philosophy**
|
||||||
|
- Prefer unit tests for pure business logic (calculations, validations, transformations)
|
||||||
|
- Use integration tests for database interactions and cross-system flows
|
||||||
|
- Use UI tests only for end-to-end happy paths that touch multiple pages
|
||||||
|
- Every behavior must be user-visible; no tests for implementation details
|
||||||
|
|
||||||
| Route | 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
|
## Testing Patterns
|
||||||
|
|
||||||
### Transaction List
|
### Pattern: Grid Page Behaviors
|
||||||
|
Most list pages in Integreat follow the same pattern:
|
||||||
|
1. Fetch IDs via Datomic query with filters
|
||||||
|
2. Hydrate results via `pull-many`
|
||||||
|
3. Render table with sortable columns
|
||||||
|
4. Support selection (individual / all / all-filtered)
|
||||||
|
5. Action buttons appear conditionally based on permissions and selection state
|
||||||
|
|
||||||
#### Grid Display
|
**Test implications:** Validate the query generation, not the rendering. UI tests only need to verify one filter and one sort work end-to-end.
|
||||||
- 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
|
### Pattern: Admin Insights Behaviors
|
||||||
- **Vendor**: Typeahead search against vendor names (fetches from `:vendor-search` endpoint)
|
Insights pages display AI recommendations for coding transactions:
|
||||||
- **Bank Account**: Radio card selector with "All" plus client's bank accounts; dynamically reloads via `clientSelected` event
|
1. Fetch unapproved transactions with `outcome-recommendation` data
|
||||||
- **Date Range**: Standard date range picker (start/end dates)
|
2. Display recommendation buttons sorted by frequency
|
||||||
- **Description**: Free-text input with 1000ms debounced search on `keyup changed`
|
3. Allow approving (coding) or rejecting recommendations inline
|
||||||
- **Amount Range**: Two money inputs (gte/lte) with "to" label between them
|
4. Use infinite scroll instead of pagination
|
||||||
- **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
|
**Test implications:** Unit test the recommendation sorting and filtering logic. Integration test the approve/reject endpoints. UI test the infinite scroll and animation behaviors.
|
||||||
- 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
|
### Pattern: Permission Gates
|
||||||
- Default 25 rows per page
|
Every mutating operation checks:
|
||||||
- Pagination controls rendered by grid helper
|
1. `assert-can-see-client` — user has access to the client
|
||||||
- Total matching count and sum of all matching amounts displayed
|
2. `assert-not-locked` — transaction date >= client locked-until or bank account start-date
|
||||||
|
3. `can?` — user has the specific permission for the activity
|
||||||
|
|
||||||
#### Actions
|
**Test implications:** Integration test each gate independently. UI tests only verify the happy path with a permitted user.
|
||||||
- "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
|
## Transaction List Page
|
||||||
|
|
||||||
|
### Display Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 1.1 | It should display a table with columns: Client, Vendor, Description, Date, Amount, Links | UI | [ ] |
|
||||||
|
| 1.2 | It should hide the Client column when only one client with one location is selected | Integration | [ ] |
|
||||||
|
| 1.3 | It should display the description from `description-original` in the Description column | UI | [ ] |
|
||||||
|
| 1.4 | It should fall back to `description-simple` in italics in the Vendor column when no vendor is assigned | UI | [ ] |
|
||||||
|
| 1.5 | It should render dates in `MM/DD/YYYY` format | UI | [ ] |
|
||||||
|
| 1.6 | It should right-align amounts and format them as `$X,XXX.XX` | UI | [ ] |
|
||||||
|
| 1.7 | It should display a links dropdown with links to associated Payment page or Client Overrides | UI | [ ] |
|
||||||
|
| 1.8 | It should show checkboxes for bulk selection on each row | UI | [ ] |
|
||||||
|
| 1.9 | It should group table rows by vendor name (or "No vendor") when sorted by Vendor | Integration | [ ] |
|
||||||
|
| 1.10 | It should show the grid title "Transaction" and entity name "register" | UI | [ ] |
|
||||||
|
| 1.11 | It should display a breadcrumb showing "Transactions" linking to the list page | UI | [ ] |
|
||||||
|
|
||||||
|
### Filtering Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 2.1 | It should filter transactions by vendor typeahead selection | Integration | [ ] |
|
||||||
|
| 2.2 | It should filter transactions by bank account via radio card selector with "All" plus client's bank accounts | Integration | [ ] |
|
||||||
|
| 2.3 | It should dynamically reload the bank account filter when the `clientSelected` event fires | Integration | [ ] |
|
||||||
|
| 2.4 | It should filter transactions by date range (start/end dates) | Integration | [ ] |
|
||||||
|
| 2.5 | It should filter transactions by description with 1000ms debounced search | Integration | [ ] |
|
||||||
|
| 2.6 | It should filter transactions by amount range (min/max) | Integration | [ ] |
|
||||||
|
| 2.7 | It should support exact-match navigation to a specific transaction by ID, bypassing other filters | Integration | [ ] |
|
||||||
|
| 2.8 | It should render the exact-match ID as a removable pill when present in query params | UI | [ ] |
|
||||||
|
| 2.9 | Given multiple filters are applied, when the user changes one filter, then the table should refresh with the combined filter set | Integration | [ ] |
|
||||||
|
|
||||||
|
### Sorting Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 3.1 | It should sort by client name ascending/descending | Integration | [ ] |
|
||||||
|
| 3.2 | It should sort by vendor name ascending/descending, handling missing vendors by grounding empty string | Integration | [ ] |
|
||||||
|
| 3.3 | It should sort by description ascending/descending | Integration | [ ] |
|
||||||
|
| 3.4 | It should sort by date ascending/descending | Integration | [ ] |
|
||||||
|
| 3.5 | It should sort by amount ascending/descending | Integration | [ ] |
|
||||||
|
| 3.6 | Given the user clicks a column header twice, then the sort direction should toggle | Integration | [ ] |
|
||||||
|
| 3.7 | It should default to ascending sort on the implicit `sort-default` field | Integration | [ ] |
|
||||||
|
|
||||||
|
### Pagination Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 4.1 | It should display 25 transactions per page by default | Integration | [ ] |
|
||||||
|
| 4.2 | It should display the total matching count and sum of all matching amounts | Integration | [ ] |
|
||||||
|
| 4.3 | It should render pagination controls via the grid helper | UI | [ ] |
|
||||||
|
|
||||||
|
### Action Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 5.1 | It should display an "Add Transaction" button (primary color) linking to the new transaction route | UI | [ ] |
|
||||||
|
|
||||||
|
### CSV Export Behaviors
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 6.1 | It should export all filtered results (not just the current page) as CSV | Integration | [ ] |
|
||||||
|
| 6.2 | It should include CSV headers: Id, Client, Vendor, Description, Date, Amount | Integration | [ ] |
|
||||||
|
| 6.3 | It should use raw data values instead of formatted display values in the CSV | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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.
|
> **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:
|
### Basic Details
|
||||||
- `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
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 7.1 | It should display a new transaction form at `GET /transaction2/new` | UI | [ ] |
|
||||||
|
| 7.2 | It should allow selecting a location via location selector sub-component | UI | [ ] |
|
||||||
|
| 7.3 | It should allow selecting an account via account typeahead sub-component | UI | [ ] |
|
||||||
|
| 7.4 | It should allow adding line items via line item sub-component | UI | [ ] |
|
||||||
|
| 7.5 | It should submit the new transaction via `POST /transaction2/new` | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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.
|
> **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:
|
### External Transaction Entry
|
||||||
- `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:
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Deduplication via SHA-256 synthetic keys
|
|---|----------|---------------|--------|
|
||||||
- Auto-matching to existing pending payments by check number or amount
|
| 8.1 | It should display an external transaction entry page at `GET /transaction2/external-new` | UI | [ ] |
|
||||||
- Auto-matching to expected deposits
|
|
||||||
- Auto-coding via transaction rules
|
|
||||||
- Categorization: `:import`, `:extant`, `:suppressed`, `:error`, `:not-ready`
|
|
||||||
|
|
||||||
### Admin Insights / Coding
|
### CSV/Text Import
|
||||||
|
|
||||||
#### Insights Page
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Admin-only page at `/transaction/insights`
|
|---|----------|---------------|--------|
|
||||||
- Title: "Transaction Insights"
|
| 8.2 | It should display an import form for CSV/text paste at `GET /transaction2/external-import-new` | UI | [ ] |
|
||||||
- Breadcrumbs: Transactions > Insights
|
| 8.3 | It should parse pasted data via `POST /transaction2/external-import-new/parse` | Integration | [ ] |
|
||||||
- Displays data grid card with title "Transaction Insights"
|
| 8.4 | It should execute the import via `POST /transaction2/external-import-new/import` | Integration | [ ] |
|
||||||
- Grid headers: Client, Account, Date, Description, Amount, Actions
|
| 8.5 | It should deduplicate transactions via SHA-256 synthetic keys during import | Unit | [ ] |
|
||||||
- No pagination; shows up to 50 recommendations at a time
|
| 8.6 | It should auto-match imported transactions to existing pending payments by check number or amount | Integration | [ ] |
|
||||||
- Infinite scroll via `hx-trigger="intersect once"` on last row
|
| 8.7 | It should auto-match imported transactions to expected deposits | Integration | [ ] |
|
||||||
- When no more recommendations, shows "That's the last of 'em!"
|
| 8.8 | It should auto-code imported transactions via transaction rules | Integration | [ ] |
|
||||||
|
| 8.9 | It should categorize imports as `:import`, `:extant`, `:suppressed`, `:error`, or `:not-ready` | Integration | [ ] |
|
||||||
|
|
||||||
#### 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
|
## Admin Insights / Coding
|
||||||
- **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
|
### Insights Page Display
|
||||||
- Fetches vector embedding for transaction ID from Pinecone index
|
|
||||||
- Queries for top 100 similar vectors
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Filters to scores > 0.95
|
|---|----------|---------------|--------|
|
||||||
- Enriches matches with vendor name and account numeric code from Datomic
|
| 9.1 | It should display the insights page at `/transaction/insights` for admin users | UI | [ ] |
|
||||||
|
| 9.2 | It should show the title "Transaction Insights" and breadcrumbs: Transactions > Insights | UI | [ ] |
|
||||||
|
| 9.3 | It should display a data grid with headers: Client, Account, Date, Description, Amount, Actions | UI | [ ] |
|
||||||
|
| 9.4 | It should show up to 50 recommendations at a time with no pagination | Integration | [ ] |
|
||||||
|
| 9.5 | It should implement infinite scroll via `hx-trigger="intersect once"` on the last row | UI | [ ] |
|
||||||
|
| 9.6 | It should display "That's the last of 'em!" when no more recommendations are available | UI | [ ] |
|
||||||
|
| 9.7 | It should show an empty grid when no unapproved transactions have recommendations | UI | [ ] |
|
||||||
|
|
||||||
|
### Recommendation Rows
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 10.1 | It should show unapproved transactions from the last 300 days that have `outcome-recommendation` data | Integration | [ ] |
|
||||||
|
| 10.2 | It should display each row with: client code, bank account code, date, description, amount | UI | [ ] |
|
||||||
|
| 10.3 | It should display the amount as a rounded dollar tag (green for positive, red for negative) | UI | [ ] |
|
||||||
|
| 10.4 | It should show up to 3 recommendation buttons per row, sorted by frequency (highest count first) | Integration | [ ] |
|
||||||
|
| 10.5 | It should display each recommendation button as `Vendor Name | Account Name` with a count badge | UI | [ ] |
|
||||||
|
| 10.6 | It should render recommendation buttons in `:primary` color if seen by client, `:secondary` otherwise | UI | [ ] |
|
||||||
|
|
||||||
|
### Coding Actions
|
||||||
|
|
||||||
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 11.1 | It should approve and code a transaction via `POST /transaction/insights/code/:id` | Integration | [ ] |
|
||||||
|
| 11.2 | It should set the approval status to `approved` and assign vendor and account when coding | Integration | [ ] |
|
||||||
|
| 11.3 | It should distribute the amount across valid locations using `spread-cents` when coding | Unit | [ ] |
|
||||||
|
| 11.4 | It should re-render the row with `live-added` class and Alpine.js disappear animation after coding | UI | [ ] |
|
||||||
|
| 11.5 | It should reject a recommendation via `DELETE /transaction/insights/disapprove/:id` | Integration | [ ] |
|
||||||
|
| 11.6 | It should clear `outcome-recommendation` on the transaction when rejecting | Integration | [ ] |
|
||||||
|
| 11.7 | It should re-render the row with `live-removed` class and disappear animation after rejecting | UI | [ ] |
|
||||||
|
| 11.8 | It should open an explain modal via `GET /transaction/insights/explain/:id` | UI | [ ] |
|
||||||
|
| 11.9 | It should display similar transactions from Pinecone vector search in the explain modal | Integration | [ ] |
|
||||||
|
| 11.10 | It should display Date, Description, Amount, Vendor, Account, and Similarity Score for similar transactions | UI | [ ] |
|
||||||
|
| 11.11 | It should filter similar transactions to scores > 0.95 | Unit | [ ] |
|
||||||
|
| 11.12 | It should show the top 10 similar transactions plus the target transaction highlighted | UI | [ ] |
|
||||||
|
| 11.13 | It should show only the target transaction with no similar matches if Pinecone API fails | Integration | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Cross-Cutting Behaviors
|
## Cross-Cutting Behaviors
|
||||||
|
|
||||||
### Approval Workflow
|
### Approval Workflow Behaviors
|
||||||
- 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
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Transactions can be coded with one or more expense accounts
|
|---|----------|---------------|--------|
|
||||||
- Account total must equal 100% of transaction amount (validated server-side)
|
| 12.1 | It should set transactions to `unapproved` status on import | Integration | [ ] |
|
||||||
- Location must match account's fixed location if one is set
|
| 12.2 | It should exclude `suppressed` transactions from all list queries including GraphQL | Integration | [ ] |
|
||||||
- Location "Shared" distributes amount proportionally across all client locations
|
| 12.3 | It should show `requires-feedback` transactions in the dashboard tasks card | Integration | [ ] |
|
||||||
- Location "A" is reserved for liabilities/equities/assets
|
| 12.4 | It should allow admin-only bulk status changes via GraphQL mutation `bulk_change_transaction_status` | Integration | [ ] |
|
||||||
- Bulk coding available via GraphQL mutation `bulk_code_transactions` (admin only)
|
| 12.5 | It should block modifying locked transactions (before `client/locked-until` or `bank-account/start-date`) | Integration | [ ] |
|
||||||
|
|
||||||
### Bank Account Filtering
|
### Coding Behaviors
|
||||||
- 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
|
| # | Behavior | Test Strategy | Status |
|
||||||
- CSV route uses same filters as table view
|
|---|----------|---------------|--------|
|
||||||
- Exports all matching rows (bypasses pagination)
|
| 13.1 | It should allow coding transactions with one or more expense accounts | Integration | [ ] |
|
||||||
- Includes ID column in CSV but not in HTML view
|
| 13.2 | It should validate that account totals equal 100% of the transaction amount server-side | Unit + Integration | [ ] |
|
||||||
|
| 13.3 | It should require the location to match the account's fixed location if one is set | Integration | [ ] |
|
||||||
|
| 13.4 | It should distribute amounts proportionally across all client locations when location is "Shared" | Unit | [ ] |
|
||||||
|
| 13.5 | It should reserve location "A" for liabilities/equities/assets | Integration | [ ] |
|
||||||
|
| 13.6 | It should allow admin-only bulk coding via GraphQL mutation `bulk_code_transactions` | Integration | [ ] |
|
||||||
|
|
||||||
### Payments & Linking
|
### Bank Account Filtering Behaviors
|
||||||
- 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
|
| # | Behavior | Test Strategy | Status |
|
||||||
- View transactions: `:activity :view :subject :transaction`
|
|---|----------|---------------|--------|
|
||||||
- Insights page: `:activity :insights :subject :transaction`
|
| 14.1 | It should show the bank account filter only when a client is selected | UI | [ ] |
|
||||||
- Bulk status change: admin only (`assert-admin`)
|
| 14.2 | It should dynamically update the bank account filter via HTMX when `clientSelected` event fires | Integration | [ ] |
|
||||||
- Bulk coding: admin only
|
| 14.3 | It should validate that the selected bank account belongs to the current client | Integration | [ ] |
|
||||||
- Edit transaction: power user, with client visibility check
|
| 14.4 | It should default to "All" if the selected account doesn't belong to the current client | Integration | [ ] |
|
||||||
- Match/unlink transactions: power user
|
|
||||||
- All SSR routes redirect unauthenticated users to `/login`
|
|
||||||
|
|
||||||
### Import Processing
|
### CSV Export Behaviors
|
||||||
- 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
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 15.1 | It should use the same filters for CSV export as the table view | Integration | [ ] |
|
||||||
|
| 15.2 | It should export all matching rows bypassing pagination | Integration | [ ] |
|
||||||
|
| 15.3 | It should include the ID column in CSV but not in the HTML view | Integration | [ ] |
|
||||||
|
|
||||||
### Empty Filter Results
|
### Payments & Linking Behaviors
|
||||||
- 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
|
| # | Behavior | Test Strategy | Status |
|
||||||
- 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
|
| 16.1 | It should auto-match transactions to payments by check number or amount on import | Integration | [ ] |
|
||||||
|
| 16.2 | It should create a cleared payment and set the transaction to `approved` with Accounts Payable account when linking | Integration | [ ] |
|
||||||
|
| 16.3 | It should revert the transaction to `unapproved` and clear payment/accounts when unlinking | Integration | [ ] |
|
||||||
|
| 16.4 | It should allow a transaction to pay multiple autopay invoices, creating a payment that clears them all | Integration | [ ] |
|
||||||
|
| 16.5 | It should allow a transaction to pay multiple unpaid invoices for outstanding balances | Integration | [ ] |
|
||||||
|
|
||||||
### Exact Match ID
|
### Permission Behaviors
|
||||||
- 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
|
| # | Behavior | Test Strategy | Status |
|
||||||
- Bank account filter area renders empty (no radio cards)
|
|---|----------|---------------|--------|
|
||||||
- All other filters still function
|
| 17.1 | It should require `:activity :view :subject :transaction` permission to view transactions | Integration | [ ] |
|
||||||
- Query uses all visible clients
|
| 17.2 | It should require `:activity :insights :subject :transaction` permission to access the insights page | Integration | [ ] |
|
||||||
|
| 17.3 | It should restrict bulk status changes to admin only | Integration | [ ] |
|
||||||
|
| 17.4 | It should restrict bulk coding to admin only | Integration | [ ] |
|
||||||
|
| 17.5 | It should require power user role with client visibility check to edit transactions | Integration | [ ] |
|
||||||
|
| 17.6 | It should require power user role to match/unlink transactions | Integration | [ ] |
|
||||||
|
| 17.7 | It should redirect unauthenticated users to `/login` for all SSR routes | Integration | [ ] |
|
||||||
|
|
||||||
### Insights with No Recommendations
|
### Import Processing Behaviors
|
||||||
- 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
|
| # | Behavior | Test Strategy | Status |
|
||||||
- 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
|
| 18.1 | It should assign client and bank account during import | Integration | [ ] |
|
||||||
|
| 18.2 | It should set initial status to `unapproved` on import | Integration | [ ] |
|
||||||
|
| 18.3 | It should extract check number from description if present during import | Unit | [ ] |
|
||||||
|
| 18.4 | It should attempt auto-match to pending payment during import | Integration | [ ] |
|
||||||
|
| 18.5 | It should attempt auto-match to expected deposit during import | Integration | [ ] |
|
||||||
|
| 18.6 | It should apply transaction rules for auto-coding during import | Integration | [ ] |
|
||||||
|
| 18.7 | It should apply default vendor if set during import | Integration | [ ] |
|
||||||
|
| 18.8 | It should deduplicate via SHA-256 of `date-bank-account-description-amount-index-client` | Unit | [ ] |
|
||||||
|
| 18.9 | It should skip suppressed transactions on re-import | Integration | [ ] |
|
||||||
|
|
||||||
### Pinecone Unavailable
|
### Empty State Behaviors
|
||||||
- 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
|
| # | Behavior | Test Strategy | Status |
|
||||||
|
|---|----------|---------------|--------|
|
||||||
|
| 19.1 | It should render an empty state when no transactions match filters | UI | [ ] |
|
||||||
|
| 19.2 | It should show `$0.00` for sum amount when no transactions match | UI | [ ] |
|
||||||
|
| 19.3 | It should render pagination controls showing 0 results when no transactions match | UI | [ ] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Test Data Requirements
|
## Test Data Requirements
|
||||||
|
|
||||||
### Users
|
| Entity | Requirements |
|
||||||
- Admin user with `:user/role "admin"`
|
|--------|-------------|
|
||||||
- Power user with access to specific clients
|
| **Users** | Admin user with `:user/role "admin"`; power user with access to specific clients; regular user with client visibility restrictions; unauthenticated user |
|
||||||
- Regular user with client visibility restrictions
|
| **Clients** | Minimum 2 clients with different locations; client with multiple bank accounts; client with single location (to test client column hiding) |
|
||||||
- Unauthenticated user (for redirect tests)
|
| **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 (`unapproved`, `approved`, `requires-feedback`, `excluded`, `suppressed`); 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 |
|
||||||
|
|
||||||
### Clients
|
## Existing Tests to Preserve
|
||||||
- Minimum 2 clients with different locations
|
|
||||||
- Client with multiple bank accounts
|
|
||||||
- Client with single location (to test client column hiding)
|
|
||||||
|
|
||||||
### Bank Accounts
|
- `test/clj/auto_ap/ssr/transaction_test.clj` — Transaction SSR routes and behaviors
|
||||||
- Bank account with `bank-account/name` and `bank-account/numeric-code`
|
- `test/clj/auto_ap/integration/routes/transaction_test.clj` — Transaction import routes
|
||||||
- Bank account with `bank-account/start-date` (to test locked transactions)
|
- `test/clj/auto_ap/integration/graphql/transactions.clj` — GraphQL transaction operations
|
||||||
- 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
|
## Dependencies
|
||||||
|
|
||||||
### External Services
|
- Datomic (primary store, history for unvoid)
|
||||||
- **Datomic**: All transaction data stored and queried via Datomic
|
- Pinecone (vector similarity search for transaction insights explain feature)
|
||||||
- **Pinecone**: Vector similarity search for transaction insights (explain feature)
|
- Solr (index updated on transaction changes via `solr/touch-with-ledger`)
|
||||||
- **Solr**: Index updated on transaction changes (`solr/touch-with-ledger`)
|
- HTMX (all SSR interactions: filtering, sorting, pagination, insights coding)
|
||||||
|
- Alpine.js (filter state for exact match pill, row disappear animations)
|
||||||
### 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