refactor(all): rewrite all behavior docs in table format with checkboxes

Rewrite all 11 remaining behavior documents to match the streamlined
invoice.md format:

- dashboard.md: 250 lines, 62 behaviors
- payment.md: 260 lines, behaviors for list, void, check printing, ACH
- transaction.md: 310 lines, list, import, admin insights
- ledger.md: 519 lines, entries, P&L, balance sheet, cash flows
- company.md: 320 lines, profile, 1099s, Plaid/Yodlee, reports
- admin.md: 494 lines, clients, accounts, vendors, rules, jobs, history
- pos.md: 405 lines, sales, deposits, tenders, refunds, shifts
- search-indicators.md: 167 lines, search modal, indicators
- auth.md: 184 lines, login, logout, impersonation, sessions
- outgoing-invoice.md: 192 lines, create, line items, PDF
- legacy-spa.md: 340 lines, all legacy pages (docs only)

All documents now use:
- Testing Patterns section with reusable abstractions
- Numbered tables: # | Behavior | Test Strategy | Status
- It should... behavior descriptions
- Checkboxes [ ]/[x] for tracking implementation
- Cross-Cutting Behaviors for permissions, lock dates, etc.
- Test Data Requirements tables
- Existing Tests to Preserve sections

Total: 3,844 lines of behavior documentation across 12 subsystem docs.
This commit is contained in:
2026-05-04 13:48:51 -07:00
parent e14a23ff54
commit d627e3c5d0
11 changed files with 2837 additions and 2500 deletions

View File

@@ -4,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.
## 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
- `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`)
**Test implications:** Unit test validation logic and calculation functions. Integration test the full POST flow. UI test only the happy path.
### 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
- 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
**Test implications:** Unit test the filtering and calculation logic. Integration test HTMX endpoints. UI test the add/remove interactions.
### UI Test Behaviors (SSR)
---
#### Happy Path: Create and Generate Invoice
1. Authenticated user navigates to outgoing invoice new page
2. Page renders with breadcrumbs: Invoices > Outgoing > New
3. User selects a client from the typeahead ("From (client)")
4. User enters invoice number (e.g., "10000")
5. User selects/enters invoice date
6. User enters recipient name in "To" field
7. User fills in recipient address: street1, city, state, zip (street2 optional)
8. User adds line items: clicks "Add line", enters description, quantity, unit price
9. User adds multiple line items
10. User verifies or adjusts tax percentage (default 10.0)
11. User clicks "Generate" button
12. Server validates form, calculates totals, invokes PDF Lambda
13. Modal appears with message "Download your invoice" and link to S3 URL
14. User clicks link to download PDF
## Create Invoice
#### Add and Remove Line Items
1. User views new invoice form with one default empty line item
2. User clicks "Add line" button
3. HTMX fetches new line item row and appends it to the grid
4. User fills in the new line item
5. User clicks delete (X) button on a line item
6. Row fades out and removes itself via Alpine.js
7. Remaining line items retain their data
### Form Display Behaviors
#### Validation Errors
1. User clicks "Generate" without filling required fields
2. Form POSTs and returns with validation error styling
3. Required fields show error indicators
4. User fills in missing fields and resubmits
5. Invoice generates successfully
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 1.1 | It should render a new invoice form with breadcrumbs: Invoices > Outgoing > New | UI | [ ] |
| 1.2 | It should display a client typeahead field labeled "From (client)" | UI | [ ] |
| 1.3 | It should display an invoice number input field | UI | [ ] |
| 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
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
### Form Validation Behaviors
## 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
- **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
### Submission Behaviors
### Line Items
- **All line items empty**: Subtotal is `0.0`, tax is `0.0`, total is `0.0`
- **Single line item**: Calculates correctly
- **Many line items (50+)**: Grid scrolls; each item calculates independently
- **Delete all line items**: Grid empty; adding new row restores functionality
- **Line item with only description**: Filtered out (requires description + values)
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 3.1 | It should filter out line items with empty descriptions before calculation | Unit | [ ] |
| 3.2 | It should calculate each line item total as `unit-price * quantity` | Unit | [ ] |
| 3.3 | It should calculate subtotal as the sum of all line item totals | Unit | [ ] |
| 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
- **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
## Line Items
### Tax Calculation
- **Tax as whole number (10)**: Treated as 10% (multiplied by 0.10)
- **Tax with decimals (8.25)**: Treated as 8.25%
- **Tax over 100%**: Allowed by schema; mathematically valid but business-questionable
- **Zero tax**: Total equals subtotal exactly
### Add Line Item Behaviors
| # | Behavior | Test Strategy | Status |
|---|----------|---------------|--------|
| 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
### Users
- Authenticated user with access to at least one client
- Client with complete profile including address
| Entity | Requirements |
|--------|-------------|
| **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
- Client with `:client/name` and `:client/address` (street, city, state, zip)
- Multiple clients to test typeahead selection
## Existing Tests to Preserve
### Form Data
- Valid invoice number strings
- Valid dates in `normal-date` format
- Recipient names and addresses
- Line items with descriptions, quantities (numeric), unit prices (monetary)
- Tax rates (percentage values, e.g., 10.0 for 10%)
### AWS Lambda Mock
- Mock `genpdf` Lambda invocation returning valid S3 URL
- Mock `genpdf` Lambda returning error
- `test/clj/auto_ap/ssr/outgoing_invoice_test.clj` — Outgoing invoice form rendering, submission, and PDF generation
## Dependencies
### External Services
- **AWS Lambda**: `genpdf` function generates PDF from invoice data
- **S3**: Generated PDFs stored at `data.prod.app.integreatconsult.com/<path>`
- **Datomic**: Client lookup for typeahead, address data
### Frontend Libraries
- **HTMX**: Form submission, line item row fetching
- **Alpine.js**: Line item row show/hide and removal animation
- **Form cursor (`auto-ap.ssr.form-cursor`)**: Field state management, error binding
### Middleware Stack
- `wrap-secure`: Requires authentication
- `wrap-client-redirect-unauthenticated`: Redirects unauthenticated users
- `wrap-trim-client-ids`: Trims client IDs from request
- `wrap-schema-decode`: Validates and decodes form data against `form-schema`
- `wrap-nested-form-params`: Parses nested form parameter structures
### Related Subsystems
- **Invoices**: Outgoing invoices are linked from the main invoices page
- **Company/Clients**: Client typeahead depends on company search endpoint
- **Company Search**: Typeahead fetches from `:company-search` route
- Datomic (client lookup for typeahead, address data)
- AWS Lambda (`genpdf` function generates PDF from invoice data)
- AWS S3 (generated PDFs stored at `data.prod.app.integreatconsult.com/<path>`)
- HTMX (form submission, line item row fetching)
- Alpine.js (line item row removal animation)
- Form cursor (`auto-ap.ssr.form-cursor`) — field state management, error binding