Add behavior documentation covering all SSR and legacy SPA pages: - Testing strategy and type definitions (unit/integration/UI) - Dashboard, Invoice, Payment, Transaction, Ledger pages - Company/Settings, POS, Admin, Search, Auth pages - Legacy SPA behavior docs (no UI tests until migrated) - Edge cases, test data requirements, and dependencies per subsystem Total: 3,600+ lines of behavior documentation to guide test authorship.
180 lines
8.6 KiB
Markdown
180 lines
8.6 KiB
Markdown
# Outgoing Invoice Behaviors
|
|
|
|
## Overview
|
|
|
|
The Outgoing Invoice subsystem allows users to create and generate PDF invoices to send to customers. It provides a form-based workflow for specifying the client (sender), recipient details, invoice metadata (date, number, tax rate), and line items. Upon submission, the system calculates subtotals, applies tax, and invokes an AWS Lambda function (`genpdf`) to generate a downloadable PDF.
|
|
|
|
## Routes & Pages
|
|
|
|
| Route | Method | Handler | Purpose |
|
|
|-------|--------|---------|---------|
|
|
| `GET /outgoing-invoice/new` | GET | `::new` | Renders the new outgoing invoice form |
|
|
| `POST /outgoing-invoice/new` | POST | `::new-submit` | Validates form, generates PDF, shows download modal |
|
|
| `GET /outgoing-invoice/line-item/new` | GET | `::new-line-item` | Returns HTML for a new empty line item row (HTMX) |
|
|
|
|
## Behaviors
|
|
|
|
### Unit Tests
|
|
|
|
- `form-schema` validates required fields: client, date, to, invoice-number, tax, to-address, line-items
|
|
- `form-schema` requires line-item description, unit-price, and quantity
|
|
- `form-schema` makes address street2 optional (nullable string)
|
|
- `form-schema` coerces line-items vector from form params
|
|
- `form-schema` applies `strip` decoder to street2 (trims whitespace, treats empty as nil)
|
|
- `line-item` renders a data-grid row with hidden db/id, description input, quantity money-input, unit-price money-input, and delete button
|
|
- `line-item` delete button uses Alpine.js to fade out and remove row after 500ms
|
|
- `submit` filters out line items with empty descriptions
|
|
- `submit` calculates line-item total as `unit-price * quantity`
|
|
- `submit` calculates subtotal as sum of all line-item totals
|
|
- `submit` calculates tax as `subtotal * tax-rate` (tax is a percentage, e.g., 10.0 = 10%)
|
|
- `submit` calculates total as `subtotal + tax`
|
|
- `submit` formats monetary values as `$X,XXX.XX` strings before sending to Lambda
|
|
- `submit` formats invoice date as `normal-date` string before sending to Lambda
|
|
- `submit` invokes `genpdf` Lambda with JSON payload
|
|
- `submit` extracts S3 URL from Lambda response and presents download link
|
|
- `fmt-money` formats nil as `$0.00`
|
|
- `fmt-money` formats large numbers with comma separators (e.g., `$1,234.56`)
|
|
|
|
### Integration Tests
|
|
|
|
- `GET /outgoing-invoice/new` returns 200 with full form HTML for authenticated user
|
|
- Form includes client typeahead, invoice number input, date picker, line items grid, tax input, and recipient address fields
|
|
- Client typeahead fetches from `:company-search` endpoint
|
|
- Date input renders with `normal-date` formatted value
|
|
- Form POSTs to `::new-submit` via HTMX
|
|
- `POST /outgoing-invoice/new` with valid data returns 200 with modal containing PDF download link
|
|
- `POST /outgoing-invoice/new` with invalid data returns form with validation errors
|
|
- Missing required fields (client, date, to, invoice-number) show validation errors
|
|
- Empty line items (no description) are filtered out before calculation
|
|
- `GET /outgoing-invoice/line-item/new` returns HTML for a new line item row
|
|
- New line item row can be appended to the line items grid via HTMX
|
|
- Multiple line items can be added and each calculates independently
|
|
- Form is wrapped with `wrap-schema-decode` using `form-schema`
|
|
- Form is wrapped with `wrap-nested-form-params` to handle nested form parameter keys
|
|
- Unauthenticated requests redirect to `/login`
|
|
- Request applies `wrap-trim-client-ids` and `wrap-secure` middleware
|
|
|
|
### UI Test Behaviors (SSR)
|
|
|
|
#### Happy Path: Create and Generate Invoice
|
|
1. Authenticated user navigates to outgoing invoice new page
|
|
2. Page renders with breadcrumbs: Invoices > Outgoing > New
|
|
3. User selects a client from the typeahead ("From (client)")
|
|
4. User enters invoice number (e.g., "10000")
|
|
5. User selects/enters invoice date
|
|
6. User enters recipient name in "To" field
|
|
7. User fills in recipient address: street1, city, state, zip (street2 optional)
|
|
8. User adds line items: clicks "Add line", enters description, quantity, unit price
|
|
9. User adds multiple line items
|
|
10. User verifies or adjusts tax percentage (default 10.0)
|
|
11. User clicks "Generate" button
|
|
12. Server validates form, calculates totals, invokes PDF Lambda
|
|
13. Modal appears with message "Download your invoice" and link to S3 URL
|
|
14. User clicks link to download PDF
|
|
|
|
#### Add and Remove Line Items
|
|
1. User views new invoice form with one default empty line item
|
|
2. User clicks "Add line" button
|
|
3. HTMX fetches new line item row and appends it to the grid
|
|
4. User fills in the new line item
|
|
5. User clicks delete (X) button on a line item
|
|
6. Row fades out and removes itself via Alpine.js
|
|
7. Remaining line items retain their data
|
|
|
|
#### Validation Errors
|
|
1. User clicks "Generate" without filling required fields
|
|
2. Form POSTs and returns with validation error styling
|
|
3. Required fields show error indicators
|
|
4. User fills in missing fields and resubmits
|
|
5. Invoice generates successfully
|
|
|
|
#### Empty Line Items Handling
|
|
1. User adds multiple line items
|
|
2. User leaves some line item descriptions blank
|
|
3. User submits form
|
|
4. Server filters out empty line items
|
|
5. PDF generates with only populated line items
|
|
6. Subtotal and total reflect only populated items
|
|
|
|
## Edge Cases
|
|
|
|
### Form Validation
|
|
- **Missing client**: Typeahead shows error state; form cannot submit
|
|
- **Missing date**: Date input shows error state
|
|
- **Missing recipient name**: "To" field shows error state
|
|
- **Missing invoice number**: Invoice # field shows error state
|
|
- **Invalid date format**: Schema rejects; form redisplays with error
|
|
- **Negative quantities**: Money input may accept negatives; calculation handles them
|
|
- **Zero unit price**: Line item total is `$0.00`; included in subtotal
|
|
- **Zero tax rate**: Tax is `$0.00`; total equals subtotal
|
|
- **Very large numbers**: Monetary formatting uses commas; Lambda payload handles as string
|
|
|
|
### Line Items
|
|
- **All line items empty**: Subtotal is `0.0`, tax is `0.0`, total is `0.0`
|
|
- **Single line item**: Calculates correctly
|
|
- **Many line items (50+)**: Grid scrolls; each item calculates independently
|
|
- **Delete all line items**: Grid empty; adding new row restores functionality
|
|
- **Line item with only description**: Filtered out (requires description + values)
|
|
|
|
### PDF Generation
|
|
- **Lambda invocation failure**: Exception propagates; user sees error (no modal)
|
|
- **Lambda returns invalid JSON**: `json/read-str` may throw; error handling depends on catch logic
|
|
- **S3 URL inaccessible**: Link is presented but may 403/404 on click
|
|
- **Very large invoice payload**: Lambda payload size limits may apply
|
|
|
|
### Permissions & Auth
|
|
- **Unauthenticated user**: Redirected to `/login?redirect-to=/outgoing-invoice/new`
|
|
- **Session expired during form fill**: HTMX POST returns `hx-redirect` to login
|
|
- **User without client access**: Client typeahead only shows accessible clients
|
|
|
|
### Tax Calculation
|
|
- **Tax as whole number (10)**: Treated as 10% (multiplied by 0.10)
|
|
- **Tax with decimals (8.25)**: Treated as 8.25%
|
|
- **Tax over 100%**: Allowed by schema; mathematically valid but business-questionable
|
|
- **Zero tax**: Total equals subtotal exactly
|
|
|
|
## Test Data Requirements
|
|
|
|
### Users
|
|
- Authenticated user with access to at least one client
|
|
- Client with complete profile including address
|
|
|
|
### Clients
|
|
- Client with `:client/name` and `:client/address` (street, city, state, zip)
|
|
- Multiple clients to test typeahead selection
|
|
|
|
### Form Data
|
|
- Valid invoice number strings
|
|
- Valid dates in `normal-date` format
|
|
- Recipient names and addresses
|
|
- Line items with descriptions, quantities (numeric), unit prices (monetary)
|
|
- Tax rates (percentage values, e.g., 10.0 for 10%)
|
|
|
|
### AWS Lambda Mock
|
|
- Mock `genpdf` Lambda invocation returning valid S3 URL
|
|
- Mock `genpdf` Lambda returning error
|
|
|
|
## Dependencies
|
|
|
|
### External Services
|
|
- **AWS Lambda**: `genpdf` function generates PDF from invoice data
|
|
- **S3**: Generated PDFs stored at `data.prod.app.integreatconsult.com/<path>`
|
|
- **Datomic**: Client lookup for typeahead, address data
|
|
|
|
### Frontend Libraries
|
|
- **HTMX**: Form submission, line item row fetching
|
|
- **Alpine.js**: Line item row show/hide and removal animation
|
|
- **Form cursor (`auto-ap.ssr.form-cursor`)**: Field state management, error binding
|
|
|
|
### Middleware Stack
|
|
- `wrap-secure`: Requires authentication
|
|
- `wrap-client-redirect-unauthenticated`: Redirects unauthenticated users
|
|
- `wrap-trim-client-ids`: Trims client IDs from request
|
|
- `wrap-schema-decode`: Validates and decodes form data against `form-schema`
|
|
- `wrap-nested-form-params`: Parses nested form parameter structures
|
|
|
|
### Related Subsystems
|
|
- **Invoices**: Outgoing invoices are linked from the main invoices page
|
|
- **Company/Clients**: Client typeahead depends on company search endpoint
|
|
- **Company Search**: Typeahead fetches from `:company-search` route
|