# Ledger Behaviors ## Overview 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 | 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 ### Ledger Entries List **Page load** - 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** - Client (hidden if only 1 client with 1 location) - Vendor (falls back to `alternate-description` if no vendor) - Source (hidden on internal ledger) - External ID (hidden on internal ledger, truncated to max-width) - Date - 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** - 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** - Available sorts: Client, Vendor, Source, External ID, Date, Amount, Account - Default sort: Date ascending - Sorting by Vendor or Source groups rows with break headers **Filtering (left sidebar)** - 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** - Default 25 entries per page - Configurable per-page - Pagination controls at bottom **CSV Export** - 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** - 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** - 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** - Client is required - Date is required and must be valid - Vendor is required - Amount must be ≥ $0.01 - Each line item must have an allowed account - Each line item must have a location belonging to the account - Debits must sum to total amount - Credits must sum to total amount - Debits and credits must each equal the journal entry amount **On save** - Generates external ID: `manual-` - 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 **Clipboard paste** - 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** - Validates all rows have required fields - Dates must be parseable - Account codes must be numeric or bank account strings - Locations must be 1-2 characters - Debits/Credits must be valid money amounts **Import validation** - 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** - Successful entries are imported - Entries with warnings are ignored (removed if previously existed) - Entries with errors block import and show error counts - Retracts existing entries by external ID before importing - Indexes imported entries in Solr asynchronously ### P&L Report **Form** - Customer multi-select typeahead (max 20, defaults to first 5 if "all") - Periods dropdown (default: year-to-date) - "Column per location" toggle - "Include deltas" toggle - Run button (HTMX PUT) - Export PDF button **Report generation** - 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** - Summary table: Sales, COGS, Payroll, Gross Profits, Overhead, Net Income - Detail table: Account-level breakdown within each category - Percent of sales calculated for each row - 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** - Generates PDF with Calibri Light font, 6pt - Uploads to S3 at `reports/profit-and-loss//.pdf` - Persists report record in Datomic - Returns modal with download link ### Balance Sheet **Form** - Customer multi-select typeahead (max 20, defaults to first 5 if "all") - Date dropdown (single date, default today) - "Include deltas" toggle - Run button (HTMX GET) - Export PDF button **Report generation** - 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** - Assets section with account detail and subtotal - Liabilities section with account detail and subtotal - Owner's Equity section with account detail and subtotal - Retained Earnings line - Delta columns between periods if enabled and multiple dates selected **Warnings** - Warns if >20 clients selected - Warns about unresolved ledger entries **PDF Export** - Generates PDF, uploads to S3 at `reports/balance-sheet//.pdf` - Persists report record in Datomic ### Cash Flows **Form** - Customer multi-select typeahead (max 20, defaults to first 5 if "all") - Periods dropdown (default: year-to-date) - Run button (HTMX PUT) - Export PDF button **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** - 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** - Warns if >20 clients selected - Warns about unresolved ledger entries **PDF Export** - Generates PDF, uploads to S3 at `reports/cash-flows//.pdf` - Persists report record in Datomic ### Investigation **Modal behavior** - 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) - Displays raw table without checkboxes - Max height 600px with scrollable content **Table behavior** - Uses same query schema as main ledger list - Supports sorting and pagination - Does not push URL state ## Cross-Cutting Behaviors ### Report generation - 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 - All three reports support PDF export via `clj-pdf` - PDFs use Calibri Light 6pt font on letter size - Uploaded to S3 data bucket with UUID-based key - Report metadata persisted to Datomic (`:report/name`, `:report/client`, `:report/key`, `:report/url`, `:report/creator`, `:report/created`) - Export returns modal with S3 download link ### Filtering and sorting - 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 - All ledger pages require authenticated user - Main ledger: `:read :ledger` - New/Edit journal entry: `:edit :ledger` - External import: `:import :ledger` + admin assertion - P&L report: `:read :profit-and-loss` - Balance sheet: `:read :balance-sheet` - 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 **Empty states** - No entries: empty table with pagination showing 0 results - No matching filters: empty table, filter pills remain - Report with no data: empty report form, no table rendered **Data locking** - Journal entries cannot be created for dates on or before client's `locked-until` date - External import rejects entries for locked dates **Unbalanced entries** - "Show unbalanced" filter computes debit/credit sums per entry and filters to mismatches - Unbalanced entries are still displayed in normal view **Account location mismatches** - 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** - Capped at 20 clients for performance - "All" clients defaults to first 5 for reports - Report names include all selected client names **Running balance cache** - `refresh-running-balance-cache` recomputes balances for dirty line items - Changing a ledger entry marks its line items and subsequent entries as dirty - Non-dirty entries are not recomputed ## Test Data Requirements **Clients** - At least 2 clients with different locations - Client with `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** - 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 - `auto-ap.ssr.ledger.common` - Shared grid page config, query schema, filtering - `auto-ap.ledger.reports` - Report aggregation and formatting logic - `auto-ap.ledger` - Running balance cache, account lookups - `auto-ap.datomic.accounts` - Account querying and clientization - `auto-ap.permissions` - Permission checks and middleware - `auto-ap.ssr.grid-page-helper` - Generic data grid behaviors - `auto-ap.ssr.components` - UI components (typeahead, date inputs, buttons) - `auto-ap.ssr.form-cursor` - Form state management - `clj-pdf` - PDF generation - `amazonica.aws.s3` - S3 upload for exports - `datomic.api` - Database queries and transactions