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.
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
Testing Patterns
Pattern: Form Submission with HTMX
Outgoing invoice forms use HTMX for asynchronous submission and partial page updates:
- Form fields are rendered server-side with validation state
- HTMX handles POST submission and swaps the response into the page
- Success responses trigger modal display with PDF download link
- Error responses re-render the form with validation errors
Test implications: Unit test validation logic and calculation functions. Integration test the full POST flow. UI test only the happy path.
Pattern: Dynamic Line Items
Line items are added and removed dynamically without page reload:
- "Add line" button fetches a new row via HTMX
- Each row has description, quantity, unit price inputs, and a delete button
- Delete uses Alpine.js to fade out and remove the row
- Empty line items are filtered out on submission
Test implications: Unit test the filtering and calculation logic. Integration test HTMX endpoints. UI test the add/remove interactions.
Create Invoice
Form Display Behaviors
| # |
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 |
[ ] |
Form Validation Behaviors
| # |
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 |
[ ] |
Submission Behaviors
| # |
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 |
[ ] |
Line Items
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
| 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 |
Existing Tests to Preserve
test/clj/auto_ap/ssr/outgoing_invoice_test.clj — Outgoing invoice form rendering, submission, and PDF generation
Dependencies
- 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