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.
8.6 KiB
8.6 KiB
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-schemavalidates required fields: client, date, to, invoice-number, tax, to-address, line-itemsform-schemarequires line-item description, unit-price, and quantityform-schemamakes address street2 optional (nullable string)form-schemacoerces line-items vector from form paramsform-schemaappliesstripdecoder to street2 (trims whitespace, treats empty as nil)line-itemrenders a data-grid row with hidden db/id, description input, quantity money-input, unit-price money-input, and delete buttonline-itemdelete button uses Alpine.js to fade out and remove row after 500mssubmitfilters out line items with empty descriptionssubmitcalculates line-item total asunit-price * quantitysubmitcalculates subtotal as sum of all line-item totalssubmitcalculates tax assubtotal * tax-rate(tax is a percentage, e.g., 10.0 = 10%)submitcalculates total assubtotal + taxsubmitformats monetary values as$X,XXX.XXstrings before sending to Lambdasubmitformats invoice date asnormal-datestring before sending to LambdasubmitinvokesgenpdfLambda with JSON payloadsubmitextracts S3 URL from Lambda response and presents download linkfmt-moneyformats nil as$0.00fmt-moneyformats large numbers with comma separators (e.g.,$1,234.56)
Integration Tests
GET /outgoing-invoice/newreturns 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-searchendpoint - Date input renders with
normal-dateformatted value - Form POSTs to
::new-submitvia HTMX POST /outgoing-invoice/newwith valid data returns 200 with modal containing PDF download linkPOST /outgoing-invoice/newwith 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/newreturns 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-decodeusingform-schema - Form is wrapped with
wrap-nested-form-paramsto handle nested form parameter keys - Unauthenticated requests redirect to
/login - Request applies
wrap-trim-client-idsandwrap-securemiddleware
UI Test Behaviors (SSR)
Happy Path: Create and Generate Invoice
- Authenticated user navigates to outgoing invoice new page
- Page renders with breadcrumbs: Invoices > Outgoing > New
- User selects a client from the typeahead ("From (client)")
- User enters invoice number (e.g., "10000")
- User selects/enters invoice date
- User enters recipient name in "To" field
- User fills in recipient address: street1, city, state, zip (street2 optional)
- User adds line items: clicks "Add line", enters description, quantity, unit price
- User adds multiple line items
- User verifies or adjusts tax percentage (default 10.0)
- User clicks "Generate" button
- Server validates form, calculates totals, invokes PDF Lambda
- Modal appears with message "Download your invoice" and link to S3 URL
- User clicks link to download PDF
Add and Remove Line Items
- User views new invoice form with one default empty line item
- User clicks "Add line" button
- HTMX fetches new line item row and appends it to the grid
- User fills in the new line item
- User clicks delete (X) button on a line item
- Row fades out and removes itself via Alpine.js
- Remaining line items retain their data
Validation Errors
- User clicks "Generate" without filling required fields
- Form POSTs and returns with validation error styling
- Required fields show error indicators
- User fills in missing fields and resubmits
- Invoice generates successfully
Empty Line Items Handling
- User adds multiple line items
- User leaves some line item descriptions blank
- User submits form
- Server filters out empty line items
- PDF generates with only populated line items
- 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 is0.0, total is0.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-strmay 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-redirectto 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/nameand:client/address(street, city, state, zip) - Multiple clients to test typeahead selection
Form Data
- Valid invoice number strings
- Valid dates in
normal-dateformat - 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
genpdfLambda invocation returning valid S3 URL - Mock
genpdfLambda returning error
Dependencies
External Services
- AWS Lambda:
genpdffunction 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 authenticationwrap-client-redirect-unauthenticated: Redirects unauthenticated userswrap-trim-client-ids: Trims client IDs from requestwrap-schema-decode: Validates and decodes form data againstform-schemawrap-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-searchroute