Skip to content

Roadmap

What's shipped, what's in flight, and what's planned. Updated as features land.

Legend

✅ Shipped — live in production · 🚧 In Progress — actively being built · 📋 Planned — designed/scheduled · 💡 Idea — on the long-term list


Recently Shipped

Feature Usage Tracking ✅

v3.68.0 adds a Feature Usage tab to the platform admin dashboard (/platform) that shows which features are actually being used across all tenant companies — turning raw adoption data into actionable insights. The system tracks ~35 meaningful action points per feature (invoice created, bill paid, report run, sync triggered, approval action, etc.) via a lightweight usage_events table (company_id, feature_key, action, timestamp, metadata_json). Every tracked action fires synchronously when the action completes — e.g., POST /api/invoices fires recordUsage('invoicing', 'invoice_created'), GET /api/reports/p-and-l fires recordUsage('reporting', 'p_and_l_run') — with optional metadata (e.g., record count, export format) for deeper analysis. The aggregation is computed live at query time per user selection. The Feature Usage tab shows per-feature adoption rates (X of Y companies have used this in the selected period), total action counts, the most common action, and is grouped by category (Invoicing & AR, Accounting, Reporting & Analysis, Integrations & Sync, Automation & AI, Expenses & Vendors, Taxes, Payments). A time-window selector (7 days / 30 days / 90 days / 1 year / all time) filters the results to the chosen lookback period. Clicking any feature row drills into a detail view: a per-action bar chart showing usage over the selected period, a per-company table showing when each company last used the feature and their action count, and a data export (CSV) of raw usage_events matching the feature+period. No schema change — actions are recorded to the new usage_events table; feature categorization is metadata in server/lib/featureUsageCategories.json. All endpoints include company_id filtering so tenant isolation is enforced. 14 new Jest tests cover the migration round-trip, usage event recording for 5+ action types (invoice/bill/report/sync/approval), aggregation correctness (company count, total actions, top action), per-feature drill-down (action chart data, per-company table), CSV export format, time-window filtering (7d / 30d / 90d / 1y works correctly), cross-company isolation, viewer/accountant 403, and the POST endpoint on action paths firing usage events. Total: 810 tests across 81 suites.

Vendor Spend Analysis Report ✅

v3.67.0 adds a dedicated Vendor Spend Analysis report at /reports/vendor-spend (Reports sidebar, feature flag vendor_spend_report, default on). Set a date range and immediately see how much you've spent with each vendor — pulling together both bill payments (AP workflow) and expense transactions tagged to a vendor into a single unified view. The summary shows total spend, number of active vendors in the period, and average spend per vendor. A top-10 bar chart gives an instant visual ranking. The full vendor table lists each vendor with bill spend, transaction spend, combined total, and percentage of total spend — with a proportional bar for quick comparison. Click any row to drill into that vendor: see the individual bills paid in the period and the expense transactions tagged to them, with a combined total. A monthly trend chart shows how vendor spending has evolved over the selected period — useful for catching seasonal cost spikes or spending drift. A CSV Export downloads the full vendor-by-vendor breakdown with all six columns for further analysis in a spreadsheet. No schema change — all data is pulled from the existing vendors, bills, bill_payments, and transactions tables. The report uses a UNION ALL query pattern (rather than FULL OUTER JOIN, which SQLite doesn't reliably support) to merge the two spend sources before grouping. 10 new Jest tests cover: empty result with no spend, bill payment spend by vendor, transaction spend by vendor, merged bills and transactions for the same vendor (correct totals for each source), sort order descending by total, percentage-of-total calculation, date range exclusion (payments outside range don't appear), vendor detail endpoint with bills and transactions, 404 for unknown vendor, and CSV content-type. Total: 795 tests across 80 suites.

Budget Overspend Alerts ✅

v3.66.0 adds automated spending alerts to the budget system. Configure an alert threshold on any budget (75%, 80%, 90%, or 100%) and the app will send a daily email whenever an account's actual spending reaches that percentage of its budgeted amount for the current period. The email lists every over-threshold account in a compact table: account name, budgeted amount, actual spend, and utilization % (orange at threshold, red at 100%). The threshold is set per-budget — a tight operations budget can alert at 80% while a looser "nice to have" budget can alert at 100%. A Send Alert Now admin button in the budget editor triggers the email on demand, handy for testing or catching up after a missed day. The alert is debounced to once per day per budget via an alert_last_sent_date column — re-running won't send duplicate emails. When no accounts are over threshold, Send Alert Now returns a "nothing to alert" response rather than sending a blank email. The feature is gated by the budget_alerts feature flag (Operations category, default on). Schema v70 adds alert_threshold_pct REAL NOT NULL DEFAULT 0 and alert_last_sent_date TEXT to budgets via safe ALTER TABLE … ADD COLUMN migrations; existing budgets default to 0 (disabled). The new threshold field is available on the New Budget form and in a persistent "Overspend Alerts" settings card inside every budget's editor view. Budget list rows show a small bell icon when an alert is configured. Cron fires at 7:30 AM daily (30 minutes after the inventory alert). 16 new Jest tests cover: migration column existence and default-0, getCurrentPeriodLabel for all three period types (monthly/quarterly/annual), getBudgetOverThresholdLines returning empty when threshold=0, returning empty when under threshold, returning correct lines when over threshold, sendBudgetOverspendAlert emailing recipients and stamping alert_last_sent_date, skipping when no recipients, runBudgetAlerts skipping already-sent-today budgets, skipping when feature flag disabled, POST creating budget with threshold, PUT updating threshold, and the send-alert endpoint returning 400 with no threshold and sent:false with no over-threshold lines. Total: 785 tests across 79 suites.

v3.65.0 embeds a Scan to Pay QR code in the lower-left corner of every invoice PDF that has an active online payment link. When PUBLIC_BASE_URL is set in your environment and the invoice has an unexpired Stripe Checkout payment link, the PDF renderer generates a QR code pointing to /pay/<token> — your client can scan it with any smartphone camera and pay instantly, no URL-sharing required. The QR only appears when a payment link is active; PDFs for paid, draft, or expired-link invoices render without it. The QR code is generated using the qrcode package (already a dependency for TOTP) and embedded as a 72×72 pixel PNG via PDFKit's doc.image() call. A small "Scan to pay online" caption sits below the image. The feature requires no schema change — it reads the existing invoice_payment_links table. Both the /api/invoices/:id/pdf route (stream to browser) and the /api/invoices/:id/email route (PDF attachment) include the QR when applicable. 6 new Jest tests cover: QR skipped when PUBLIC_BASE_URL is not set, PDF succeeds when no payment link exists, PDF succeeds when only an expired link exists, PDF includes QR when active link + PUBLIC_BASE_URL are both present, PDF 404 for unknown invoice, and renderInvoiceToPdf works without crashing when qrImageBuffer=null. Total: 769 tests across 78 suites.

Recurring Invoice Auto-Email ✅

v3.64.0 adds an Auto-email invoice to client toggle to recurring invoice templates. When enabled, each invoice issued by the daily cron (or by the Issue Now button) is automatically emailed to the client as a PDF attachment — no manual step required. The email uses the same SMTP configuration and invoice email template as the manual Send via Email button, including any custom subject / body templates you've configured in Settings → Email Templates. If the client has no email address, or if the email send fails for any reason (e.g. SMTP misconfigured), the failure is logged to the server console but does not block the invoice from being created — the invoice is always saved first. The template list table now shows a mail icon (📧) alongside the lightning bolt for templates with auto-email enabled. Schema v69 adds auto_send_email INTEGER NOT NULL DEFAULT 0 to recurring_invoices via a safe ALTER TABLE … ADD COLUMN migration; existing templates default to 0. The renderInvoiceToPdf helper is now exported from invoices.js so recurring issuance can generate a PDF buffer without duplicating the rendering logic. 8 new Jest tests cover: migration column existence, default-0 on existing templates, issuance without auto-email (mailer not called), issuance with auto-email (mailer called once, correct to + attachment), issuance when client has no email (no error thrown, mailer not called), email failure does not prevent invoice creation (invoice status = sent), POST creates template with auto_send_email=true, and PUT updates the flag. Total: 763 tests across 77 suites.

Inventory Reorder Alerts ✅

v3.63.0 adds automated low-stock notifications to the inventory management system. When any product's quantity on hand drops to or below its reorder point, three things happen: (1) the Dashboard shows a yellow banner — "N items below reorder point" — that links directly to a filtered inventory view; (2) the Inventory page gains a Low stock (N) toggle button in the header that filters the table to only show items needing attention; (3) a daily 7 AM cron sends a formatted email alert listing every low-stock item (name, SKU, qty on hand, reorder point) to your configured notification recipients (falling back to admin user emails). The email uses the same dark-theme brand styling as the daily digest. A Send Low Stock Alert Now admin button (POST /api/inventory/send-reorder-alert) is also available for triggering the email on demand. The feature is gated behind the inventory_alerts flag (Operations category, default on) so companies that don't use inventory tracking can keep the notification quiet. Dashboard banner and Inventory page filter work client-side without any schema changes — the data is already available from the existing GET /api/inventory response (low_stock_count) and the new GET /api/inventory/low-stock route. 11 new Jest tests cover: getLowStockItems returning only at-or-below-threshold items, skipping items with no reorder point, skipping inactive items, sendLowStockAlertForCompany skipping when the feature flag is disabled, skipping when no low-stock items exist, skipping when no recipient is configured, successful send (mailer called once, subject contains "Low Stock", HTML contains item names), recipient resolution preferring notifications_recipients over admin email, GET /inventory/low-stock returning correct items, POST /send-reorder-alert triggering the send, and POST /send-reorder-alert returning 403 for non-admin roles. Total: 755 tests across 76 suites.

Email Templates ✅

v3.62.0 lets admins customize the subject line and email body sent when using Send via Email on invoices or estimates. Go to Settings → Email Templates and click Edit on the Invoice or Estimate template. The subject and body support substitution — click any variable chip in the editor to insert it at the cursor. Available variables for invoices: , , , , , ; for estimates: , , , , , . Each company starts on the built-in default templates; click Reset on any type to discard custom changes and revert. Templates persist per-company in the new email_templates table and are used automatically by POST /api/invoices/:id/email and POST /api/estimates/:id/email. Schema v68 adds email_templates (company_id, template_type CHECK IN invoice/estimate, subject, body_html; UNIQUE per company+type). server/lib/emailTemplates.js exports DEFAULT_TEMPLATES, renderTemplate(template, vars), and getTemplate(db, companyId, type). Feature flag email_templates (Customers & Revenue, default on). New settings routes: GET /api/settings/email-templates (returns all types with is_default: true flag when no custom exists), PUT /api/settings/email-templates/:type (admin, upserts), DELETE /api/settings/email-templates/:type (admin, resets to default). 12 new Jest tests cover: migration table existence, renderTemplate substitution and unknown-variable pass-through, getTemplate returning default vs. saved, GET returning defaults with is_default flag, PUT saving correctly, GET showing custom as non-default, DELETE resetting, invalid-type 400, viewer-role 403, and upsert idempotency. Total: 744 tests across 75 suites.

SMTP Invoice & Estimate Delivery ✅

v3.61.0 lets you send invoices and estimates directly from Geekonomics as email attachments — no copy-pasting, no opening a separate mail client. Click the Mail icon on any invoice or estimate row and the app generates a branded PDF, attaches it, and sends it to the client's email address in one step. The email uses your configured company SMTP settings (from Settings → SMTP) or the platform SMTP env vars as a fallback. Sending a draft invoice or estimate auto-advances its status to sent so your workflow stays accurate without a manual status click. If the client record has no email address, or SMTP isn't configured, the button returns a clear error rather than silently failing. On the client side the old mailto: workaround (which opened your default mail app pre-filled with text and required you to manually attach the PDF) is replaced with a single API call — POST /api/invoices/:id/email and POST /api/estimates/:id/email. Invoice PDF generation was refactored to share a renderInvoiceToPdf() helper between the existing stream-to-browser PDF route and the new buffer-for-attachment route, so both always render identically. 10 new Jest tests cover: 404 on unknown invoice/estimate, 400 when client has no email (both invoice and estimate), successful send (mailer called once, correct to, subject contains invoice/estimate number, attachment is a Buffer named Invoice-XXX.pdf / Estimate-EST-XXX.pdf), draft→sent status advancement on email send, non-draft status unchanged, and 400 with SMTP error message when mailer returns not_configured. Total: 732 tests across 74 suites.

Automated Daily Digest Email ✅

v3.60.0 adds a morning financial briefing you can enable per-company. Go to Settings → Daily Digest Email, toggle it on, and at 8 AM UTC each day Geekonomics will email a snapshot of the previous day's activity. The digest covers six sections: Yesterday's P&L (revenue, expenses, net, and transaction count); Cash & Balances (cash on hand, accounts receivable, AP); Open Receivables (open invoice count + amount, overdue count + amount); Open Payables (open bill count + amount); and a Yesterday's Activity section when new invoices or bills were created. Email goes to the recipients configured in Event Notifications, falling back to all admin-user email addresses when no recipients are configured. The digest is HTML-formatted with the same dark-theme brand styling as the rest of the app. A Send Yesterday's Digest Now button in Settings lets admins trigger a manual send to test the email or catch up after a missed day. Schema v67 adds digest_enabled INTEGER DEFAULT 0 and digest_last_sent_at TEXT to companies. Feature flag daily_digest (Reports group, default on). New server/lib/digest.js exports buildDigestData, buildDigestHtml, sendDigestForCompany, and runDailyDigest. 12 new Jest tests cover: migration (columns present, default 0), data builder (zeros for empty day, revenue/expenses from GL, overdue invoice count, open bill count), HTML output (key figures present), send-disabled skip, no-recipients skip, successful send (email called, last_sent_at stamped), GET /api/settings includes both fields, PUT /api/settings/digest toggles the column. Total: 722 tests across 73 suites.

Outbound Webhook Triggers ✅

v3.59.0 adds configurable outbound HTTP webhooks so you can connect Geekonomics to Zapier, Make, n8n, or any HTTP endpoint — no polling required. Go to Settings → Outbound Webhooks (admin, Sync & Import group) and add a webhook: give it a name, paste in the endpoint URL (e.g. your Zapier catch-hook URL), and pick which events to subscribe to. Supported events: invoice.created, invoice.sent, invoice.paid, bill.created, bill.paid, customer_payment.received, transaction.created. Each webhook fires a POST request with a JSON body { event, timestamp, data } where data is a compact summary of the triggering entity (id, total, client/vendor id, date). A signing secret is auto-generated per endpoint and shown in the expanded webhook card — include it in your receiving endpoint to verify authenticity via the X-Geekonomics-Signature header (HMAC-SHA256 of the raw request body: sha256=<hex>). The Send Test (⚡) button fires a test invoice.created payload immediately so you can verify your pipeline without creating real data. Every delivery attempt is recorded in webhook_deliveries — expand any webhook card in the UI to see the last 10 deliveries with status (delivered / failed), HTTP response code, and timestamp. Delivery failures are recorded but never block the triggering operation — a broken endpoint can't prevent an invoice from saving. Schema v66 adds outbound_webhooks (company_id, name, url, secret, events JSON, is_active) and webhook_deliveries (webhook_id, event, payload_json, status, http_status, response_body, attempts, delivered_at). Feature flag webhooks (Sync & Import, default on). 13 new Jest tests cover: migration, POST/GET/PUT/DELETE CRUD, unknown-event rejection, delivery count on list, cross-company 404, signPayload HMAC consistency, triggerEvent dispatching fetch with correct headers, event-not-subscribed no-fire, and failed fetch recording the error message in delivery row. Total: 710 tests across 72 suites.

Multi-Currency Support ✅

v3.58.0 adds full multi-currency bookkeeping so companies can invoice in foreign currencies, receive bills in foreign currencies, and record bank transactions in any currency — while keeping the General Ledger always in the company's base currency. Go to Settings → Currency to configure your base currency (default USD) and add exchange rates for each foreign currency you use. Exchange rates are stored per-company with a date, so you can record historic rates for accurate backdated entries. When creating an invoice, bill, or transaction, a Currency selector appears whenever you have more than one currency configured; select the foreign currency and enter (or auto-fetch) the current exchange rate. All journal entry lines are automatically converted to base currency at posting time — amount × exchange_rate — so existing P&L, Balance Sheet, Trial Balance, and custom reports remain correct with no changes. When a bill or invoice is paid at a different exchange rate than when it was first posted (the rate moved between invoice date and payment date), the system automatically posts an FX Gain/Loss journal entry for the difference to GL account 7000 (FX Gain/Loss, a new system revenue account added to the default Chart of Accounts). Schema v65 adds a global currencies table (code, name, symbol — seeded with 18 common currencies: USD, EUR, GBP, CAD, AUD, JPY, CHF, CNY, HKD, SGD, SEK, NOK, DKK, NZD, MXN, BRL, INR, ZAR), a per-company exchange_rates table (company_id, currency_code, rate_date, rate — UNIQUE per company+currency+date), base_currency TEXT DEFAULT 'USD' on companies, and currency_code + exchange_rate columns on transactions, invoices, bills, customer_payments, and bill_payments. Feature flag multi_currency (Operations group, default on). 16 new Jest tests cover: migration (tables + column existence + seeded currencies), GET /api/currencies (is_base flags), POST exchange rate, GET /latest/:code (returns latest or 1.0 fallback), base-currency rate rejection, unknown currency rejection, GET /api/settings includes base_currency, PUT /api/settings/currency saves + validates, unknown currency code 400, bill JE in base currency (100 EUR × 1.08 = 108 USD), FX gain/loss JE posts when payment rate differs from bill rate, FX gain on customer payment, FX GL account seeded at code 7000, getSystemAccount finds fx_gain_loss by subtype. Total: 697 tests across 71 suites.

Slack / Discord Webhook Notifications ✅

v3.57.0 wires event notifications into Slack and Discord channels via incoming webhook URLs — no API keys, no bot tokens, no OAuth. Go to Settings → Event Notifications and drop in a Slack Incoming Webhook URL, a Discord Webhook URL, or both. Any event you've already checked (invoice paid, bill overdue, low stock, period locked, large JE, bill awaiting approval, feedback submitted) now fires a message to those channels alongside the usual email. The message text is the same subject line the email would use, so notifications are easy to scan in a channel feed. Webhook delivery failure is silently swallowed — a flaky Slack endpoint never blocks the operation that triggered it. The feature extends the existing notify() helper: the getRecipients() path now also reads slack_webhook_url and discord_webhook_url from the companies row, and the notify() function fires fetch POSTs after the email loop with { text: subject } as the body (Slack's incoming webhook format; Discord accepts the same shape as { content: subject } — both work with the text key as Discord aliases it). Schema v64 adds slack_webhook_url TEXT and discord_webhook_url TEXT columns to companies. Feature flag slack_notifications (Sync & Integrations, default on). Existing PUT /api/settings/notifications was updated to accept and persist the two new URL fields alongside the existing recipients and events. 11 new Jest tests cover the migration (columns present), GET returning both webhook URL fields, PUT saving them, empty-string PUT clearing them to NULL, non-admin 403, Slack webhook POST verification (correct URL + { text } body), Discord webhook POST, both firing when both configured, event-not-subscribed no-fire, and webhook-fetch-throws-does-not-propagate. Total: 681 tests across 70 suites.

Custom Report Builder ✅

v3.56.0 ships a user-defined report system at /reports/custom (Reports sidebar, feature flag custom_reports, default on). Define a report once — give it a name, optionally a description, pick any set of GL accounts from a searchable grouped picker, and choose whether to break data down by month, quarter, or year. Saved reports persist per-company and can be edited or deleted at any time. To run a report: set a start and end date on the reports list page and click Run on any saved report. The results table shows one row per GL account with values for each period column, color-coded by account type (revenue = green, expense = red, asset = blue, liability = yellow, equity = purple) — plus a Total column on the right. A grand totals row runs along the bottom. Sticky first column keeps the account name in view while scrolling through many periods. Empty periods show a dash rather than zero to avoid visual noise. CSV Export downloads the full table with Code, Account, Type, all period columns, and Total. Reversal journal entries are excluded from the computations so voided transactions don't inflate the numbers. Schema v63 adds saved_reports (name, description, account_ids JSON, group_by CHECK monthly/quarterly/annual, created_by). No separate period table — all GL data is queried live from journal_lines at run time via a GROUP BY on strftime('%Y-%m', je.date) (monthly), %Y-Q<n> (quarterly), or %Y (annual). Sign convention: positive amounts reflect the account's normal-balance direction (revenue and liabilities show positive when they have credit balances; assets and expenses show positive when they have debit balances). 14 new Jest tests cover the migration, CRUD (create, update, delete, admin-only delete), input validation (missing name, empty accounts, invalid group_by), run with monthly period breakdown, reversal exclusion, cross-company isolation, and CSV content-type + data. Total: 670 tests across 69 suites.

Session Management ✅

v3.55.0 adds a per-user Active Sessions page at /settings/sessions (Admin sidebar, feature flag session_management, default on, accessible to any authenticated user — admins and non-admins alike since it only shows your own sessions). The page lists every login recorded for your account in the last 30 days: device type (mobile vs browser), browser name, IP address, full user-agent string (truncated), and when the session was created. Your current session is tagged with a Current session badge. Any unfamiliar session can be revoked with a single click — this marks the session as revoked in the user_sessions table and calls sessionStore.destroy(sid) to immediately invalidate the server-side cookie. A Revoke All Other Sessions button at the top of the list handles the "I think my account is compromised" scenario in one click. Each successful login now inserts a row into the new user_sessions table, capturing the session ID, user agent, and IP at login time. Schema v62 adds user_sessions (company_id, user_id, session_sid UNIQUE, user_agent, ip_address, created_at, last_active_at, is_revoked). No changes to existing auth flow — session recording is a passive insert inside finalizeLogin. 11 new Jest tests cover the migration, list (empty state, non-revoked filter, cross-company isolation, is_current flag), single-session revoke (success, 404 unknown, 400 already-revoked, 404 cross-company), and revoke-all-others (marks revoked + returns count). Total: 656 tests across 68 suites.

Activity per User Dashboard ✅

v3.54.0 adds an admin-only page at /admin/activity (Admin sidebar group, feature flag activity_dashboard, default on) that shows who did what in the last 7, 30, or 90 days — pulled directly from the existing audit_log table with no schema change. The overview shows summary cards (total actions, active users, total tracked users) and a card-grid with one tile per user: their action count for the selected period, a bar-chart sparkline of daily activity, their most recent active date, and their top action type. Click any user card to drill into their detail view: a full daily-activity bar chart, an action-type breakdown with proportional bar visualization, a top-entity-types tag list, and a scrollable table of their last 100 actions in the period with action badge, entity type + ID, detail snippet, and timestamp. The period selector (7 / 30 / 90 days) re-queries both the overview and the detail view on change. Admin-only — viewer or bookkeeper roles get 403. No schema change. 11 new Jest tests cover: empty-log base case, per-user action count summary, days-filter parameter, last_active_at set to most recent entry, daily_counts aggregation by date, user-not-found 404, detail action breakdown and entity breakdown, recent_entries list, viewer 403, and cross-company isolation (both overview and per-user). Total: 645 tests across 67 suites.

Estimate E-Signature ✅

v3.53.0 closes the estimate-to-invoice loop by letting clients sign quotes in-browser without any third-party e-sign service. From the Estimates page, click the link icon on any draft, sent, or accepted estimate — a 30-day signature link is generated and copied to your clipboard. Share it however you want: paste into email, Slack, or a client portal message. Your client opens the link in any browser (no login required) and sees the full estimate: company logo, line items, totals, notes, and terms. They type their name, optionally their email, draw their signature using a finger or mouse on an HTML5 canvas, and click Accept Estimate. On submit, the signature (base64 PNG), name, email, and IP address are recorded, the estimate status is flipped to accepted, and the confirmation screen shows immediately. Back in the app the estimate is now accepted and ready to convert to an invoice with a single click. The link is idempotent — generating it twice returns the same token until it expires or is signed. If the link has already been signed, it shows an "Already Signed" confirmation page. Expired or invalid tokens show a clear error. Clicking the link button on a draft estimate auto-advances it to sent first. Schema v61 adds estimate_signature_tokens (token, estimate FK, expires_at, signed_at, signer details, signature_data). No new dependencies — signature capture uses the browser's native Canvas API. 12 new Jest tests cover the migration, token generation, token reuse, draft-to-sent advancement, declined/expired/converted rejection, public estimate load, unknown-token 404, already-signed indicator, full sign submission (status + DB update), missing-name 400, already-signed 409, and cross-company isolation. Total: 634 tests across 66 suites.

Recurring Bill Templates ✅

v3.52.0 adds scheduled AP: create a template for any recurring vendor obligation — office rent, SaaS subscriptions, retainer agreements — and the app auto-posts a new bill on each due date. Templates carry a vendor, a frequency (weekly / biweekly / monthly / quarterly / annually), a next-bill date, a configurable due-date offset (days after the bill date), optional project and class tags, and a set of line items (each with description, category, qty, and rate). Bills created from templates follow the same GL path as manually entered bills — the issuance journal entry posts Dr Expense / Cr AP immediately, and the bill enters the standard open → partial → paid status lifecycle. If your company has the bill approval workflow enabled and the template total meets the threshold, the bill is inserted as approval_status=pending and the JE is held until an approver acts. A 1:30 AM daily cron scans all active templates across all companies and issues any whose next_bill_date is today or earlier; period-locked dates are skipped without error. The Issue Now (⚡) action in the UI lets you trigger a bill immediately, which is useful for back-filling missed periods or testing a new template. Templates can be paused / resumed without losing the next-bill-date state. Schema v60 adds recurring_bills and recurring_bill_line_items. Feature flag recurring_bills (Vendors group, default on). 11 new Jest tests cover the migration, POST/PUT/DELETE CRUD, frequency validation, missing-line-items rejection, issue-now bill creation + next-date advance, empty-template 400, cron (runDueRecurringBills issues due templates and ignores future ones), is_active toggle, and cross-company isolation. Total: 622 tests across 65 suites.

Expense Policy Enforcement ✅

v3.51.0 ships rule-based spend controls at /expense-policies (AI & Automation sidebar, feature flag expense_policies, default on). Define as many policies as your business needs — each has a rule type, a dollar threshold, optional category and vendor filters, and a severity (Warning or Violation). Four rule types: Amount Limit — flags any single expense that exceeds the threshold (e.g. "no meals over $80"); Receipt Required — flags expenses above the threshold that have no receipt attached; Note Required — flags expenses above the threshold that have no description or business justification; Monthly Cap — flags the month when the running total for a category/vendor exceeds the cap (e.g. "travel can't exceed $1,000/month"). Policies can be paused and resumed without losing their violation history. Click Run Scan to check the last 90 days of expenses against all active policies. The scan is idempotent — running it twice produces the same violations, not doubles (enforced by a UNIQUE constraint on (policy_id, transaction_id)). New violations appear on the Violations tab with status filter buttons (open / acknowledged / waived). Each open violation shows the policy name, the transaction date and amount, a detail string explaining why it fired, and the severity badge. Acknowledge moves it to review status; Waive (with optional reason) closes it as an exception. Schema v59 adds expense_policies and expense_policy_violations tables. No journal entries — this is a metadata layer over existing transactions. 13 new Jest tests cover the migration, CRUD (POST creates policy with category join, PUT updates, DELETE cascades violations), input validation (invalid rule_type rejects with 400, negative threshold rejects), all four scan rule types, scan idempotency, cross-company isolation, and acknowledge status change. Total: 611 tests across 64 suites.

KPI Dashboard ✅

v3.50.0 ships a key financial metrics page at /reports/kpi (Reports sidebar, feature flag kpi_dashboard, default on). A period picker (last 12 months) drives all computations. The page is organized into five sections: Income Statement (Revenue, Gross Profit, Total Expenses, Net Income with margin %) — all with MoM trend arrows; Year to Date (YTD Revenue, Expenses, Net Income for the calendar year through the selected month); Liquidity (Cash on Hand, Accounts Receivable, Accounts Payable, Working Capital); Ratios (Current Ratio, Quick Ratio, DSO, DPO); Margins (Gross Margin %, Net Profit Margin %). All figures are computed live from journal_lines via gl_accounts type/subtype — no denormalized caches. Revenue = net credits on revenue accounts; Expenses = net debits on expense accounts; Cash = net debit balance of bank-subtype assets; AR = ar-subtype assets; AP = ap-subtype liabilities. Current Assets = Cash + AR + Inventory; Current Liabilities = AP + Sales Tax Payable + Payroll Liability. Reversal JEs (reverses IS NOT NULL or reversed_by IS NOT NULL) are excluded. MoM % compares current period to prior month. DSO = AR / (Revenue / 30); DPO = AP / (COGS or Expenses / 30). KPI cards with upward/downward trend badges (green = improvement direction). Metric definitions and period disclaimer note at page bottom. No schema change. 10 new Jest tests cover: zero-JE base case, revenue+expense computation, gross profit with COGS, out-of-period exclusion, all-time balance sheet figures, MoM % change, YTD aggregation, DSO arithmetic, default-to-current-month, and cross-company isolation. Total: 598 tests across 63 suites.

Natural Language Reports ✅

v3.49.0 adds an AI-powered financial query page at /reports/ai-query (AI & Automation sidebar, feature flag nl_reports, default on). Type a plain-English question — "Show me revenue by client for Q2 2026", "What are my top expense categories this year?", "Which vendors did I pay the most in Q1?" — and Claude translates it into a structured query, executes it, and renders the results as a titled data table inline, all in one round trip. The feature uses a typed propose_report tool — Claude never writes SQL; it fills a structured schema (table, group_by, measure, filters, order_by) and the server builds and executes the SQL from a curated whitelist. Security model: only three tables are exposed (transactions, invoices, bills) and only named, whitelisted fields within each. Every query is forced to include company_id = ? — it's never in Claude's hands. Fields like password_hash, totp_secret_enc, api_tokens, company_credentials cannot appear even if Claude were to hallucinate them, because the builder throws on any unknown field or table. A hard LIMIT 1000 is enforced. assertWithinBudget gates the Claude call; any budget overflow returns HTTP 429. The result table shows grouping columns (e.g. client, category, month) plus a formatted value column labeled Total / Count / Average based on the selected measure. Currency values are formatted as $X,XXX.XX; a footer row shows the grand total when the measure is a sum or average. Token usage is logged to ai_insights_log (type chat). Five example prompt chips on the page let users click to run common queries instantly. No schema change. 16 new Jest tests cover all SQL builder paths: group-by with join, date range filters, type filter, invoices with client join, bills using total as amount, count measure, avg_amount measure, value_asc order, contains filter with LIKE wildcards, unknown table rejection, unknown field rejection, invalid op rejection, forced company_id on all queries, plus the route integration tests (full round-trip with mocked Anthropic SDK, empty query 400, missing API key 503). Total: 588 tests across 62 suites.

Bank Rules Multi-Condition Splits ✅

v3.48.0 extends the categorization rules engine from simple description-matching into a full multi-condition, split-action system. Any rule can now carry extra AND conditions beyond the description match — filter by amount > 500, amount <= 100, or type = expense to ensure the rule only fires when all conditions are true. Rules can also be flipped into split mode: instead of assigning a single category, a split rule distributes the transaction amount across two or more categories by percentage. Set 60% to "Rent" and 40% to "Utilities" and every matching expense automatically gets two transaction_splits rows with the correct dollar amounts. The last split bucket absorbs any penny rounding so splits always sum exactly to the transaction total. When a split rule fires during Apply All Rules, the engine inserts the splits, then reverses the old journal entry and reposts a balanced, multi-line JE — one bank line and N category lines — so the General Ledger stays correct immediately, not just the transaction metadata. Simple (non-split) rules also now reverse-and-repost their JE when an account is linked, closing a long-standing GL-staleness gap. Schema v58 adds is_split to categorization_rules, a rule_conditions table (rule_id, field, operator, value), and a rule_split_actions table (rule_id, category_id, pct). Both new tables use ON DELETE CASCADE so deleting a rule cleans up its conditions and actions automatically. In the UI the rule form gains a conditions panel (+ Add Condition rows with field / operator / value dropdowns that constrain to valid combinations — type only allows eq, amount allows gt/gte/lt/lte/eq) and a Split Mode toggle that swaps the category/vendor fields for a split-lines editor with a live percentage-sum indicator that turns red until total = 100%. The rules table shows split rules with a scissors icon and renders each split line (pct → category) in the Sets column. 14 new Jest tests cover the migration round-trip, GET including conditions and split_actions, POST/PUT saving and replacing extras, invalid pct sum rejection, invalid condition field rejection, split rule creating correct transaction_splits, last-bucket rounding correctness, amount gt condition firing / not-firing, type eq condition isolating to one direction, idempotency (already-split transaction skipped), cross-company isolation, and the JE reverse+repost on both split and simple rule paths. Total: 571 tests across 61 suites.

AI Cash Flow Forecast Enhancement ✅

v3.47.0 extends the existing 90-day cash flow forecast with prior-year seasonality analysis and an AI-generated insight panel. The forecast page now shows a grouped bar chart comparing this window's projected inflows and outflows (month by month) against the same period's actual transactions from the prior year — solid bars for the forecast, lighter bars for the historical baseline — so you can immediately see whether a slow July or a bumpy October is a seasonal pattern or something new this year. The backend queries prior-year transactions for the same date range (shifted back exactly one calendar year to avoid leap-year skew), aggregates income and expense by month, and aligns the two series on the same X axis. When ANTHROPIC_API_KEY is configured, Claude reviews both monthly arrays and returns 2-3 actionable insight strings that surface specifics: which month looks light relative to last year, whether a big outflow cluster is recurring, whether the net trend is improving or deteriorating. The notes render in a numbered AI Seasonality Insights card with a disclaimer. The AI call is fully graceful — any error (network, budget exceeded, parse failure) results in an empty notes array without affecting the rest of the forecast response. Token usage is logged to ai_insights_log (type chat) and counts against the daily AI budget. No schema change. 7 new Jest tests cover the new response fields (forecast_monthly, historical_monthly, seasonality_notes), prior-year month shifting to current year, AI notes populated from mock, AI errors swallowed gracefully, ANTHROPIC_API_KEY gating, forecast monthly aggregation from invoice events, and token logging. Total: 557 tests across 60 suites.

AI Auto-Reconciliation ✅

v3.46.0 adds AI-powered bank statement import to the Bank Matches workflow. From the Bank Matches page (/bank-matches) an admin or bookkeeper clicks Import Statement, selects the bank account, and drops in a PDF or CSV bank statement. For PDF files the statement is sent to Claude, which extracts every individual transaction (date, description, amount, type) regardless of format quirks in the bank's PDF layout. For CSV files the importer auto-detects the column structure — it handles single Amount column (negative = debit), separate Debit/Credit columns, and common date formats including MM/DD/YYYY and YYYY-MM-DD. Both paths skip zero-amount rows, opening/closing balance rows, and deduplicate against transactions already imported for that account (same date + amount + description), so uploading the same statement twice is safe. After inserting new source='import' transactions, the existing deterministic matcher runs automatically — any new bank row with a same-date ±3 days / same-amount ±$0.01 match against manual transactions, transfers, bill payments, or customer payments gets a suggested entry in bank_matches right away. For rows the deterministic matcher misses, an optional AI deep match pass (enabled by default when ANTHROPIC_API_KEY is set) calls Claude with the unmatched bank rows and nearby app transactions. Claude scores matches by description similarity with a wider window (±7 days, ±$2) and returns scored candidates — "AMZN MKTP US" → "Amazon office supplies" or "SQ * COFFEE SHOP" → "Square payment" type inferences. Candidate scores are saved back to bank_matches at status='suggested' alongside the deterministic ones, so the bookkeeper reviews everything in the same queue they already use. Token usage is logged to ai_insights_log for both the PDF extract and the deep-match pass, counting against the daily AI budget. The import result summary shows how many transactions were imported, how many were skipped as duplicates, how many deterministic matches were proposed, and how many AI suggestions were added. No schema change — the existing bank_matches and transactions tables handle everything. 18 new Jest tests cover CSV parsing (single-amount format, debit/credit format, MM/DD/YYYY dates, zero-row skip, bad-column errors), the import endpoint (creates source=import transactions, deduplication on re-upload, account_id required, cross-company isolation, proposeMatches called, proposed count returned), PDF extraction via mocked Claude, PDF parse failure 400, and the AI deep-match call firing for unmatched rows. Total: 550 tests across 59 suites.

Schedule C / 1120-S Tax Export ✅

v3.45.0 generates year-end tax preparation worksheets for the two most common small-business filing forms. From the new Tax Export page (Payroll & Tax group, /tax/export) an admin or accountant picks a form type — Schedule C (sole prop / SMLLC) or Form 1120-S (S-Corp) — and a tax year. The report auto-assigns each of your GL revenue and expense accounts to the closest form line using subtype + name-fragment heuristics: revenue subtype → Line 1 (Gross receipts), cogs → Line 4 / Line 2, depreciation_expense → Line 13, payroll_expense → Line 26 / Line 8, accounts named "rent" → Line 20b / Line 11, "utilities" → Line 25, etc. The left panel shows every GL account with a dropdown to override the auto-assignment (or exclude the account entirely); saving stores the overrides in the new tax_form_mappings table and they persist for future runs. The right panel shows the live report — each form section (Income / Expenses for Schedule C; Income / Deductions for 1120-S) with line numbers, labels, contributing GL accounts, and line totals, plus a highlighted Net Profit / (Loss) summary. Any accounts that couldn't be mapped are called out in an amber warning block. PDF export produces a clean multi-page prep packet with the form name and year in the header band, per-section subtotals, and a prominent "tax preparation worksheet only" disclaimer. CSV export gives one row per GL account per form line with all amounts for use in spreadsheets or tax software import. Both exports are gated to requireReadAndExport (admin + bookkeeper + accountant). Schema v57 adds tax_form_mappings (company_id, form_type, gl_account_id, line_number, UNIQUE per company+form+account). Feature flag tax_export (taxes category, default on). 15 new Jest tests cover the migration, auto-mapping accuracy (revenue→line 1, depreciation→line 13, wages→line 26), net income arithmetic, custom mapping override, prior-year exclusion, mapping save-and-replace, viewer 403, and PDF + CSV response types. Total: 532 tests across 58 suites.

Check Printing ✅

v3.44.0 ships printable paper check generation for bill payments. When recording a payment on any open bill, check the Print check for this payment option — the app allocates the next sequential check number from your check stock sequence, saves it against the payment, records the JE normally, and then opens a print-ready PDF in a new tab. Every existing payment also gets a printer icon in the payments table to reprint the check at any time. The check PDF is a full-page U.S. letter document respecting your configured check stock layout (top / middle / bottom check position): the check area draws the payer name and address (from your company branding), date, "Pay to the Order of" payee line with the vendor name, an amount box with numerals, the amount spelled out in words (e.g. One Thousand Two Hundred Thirty-Four and 56/100 Dollars), a memo line pre-filled with the bill number, and a signature line. Two detachable stubs occupy the remaining two-thirds of the page showing date, payee, memo, check number, and amount. If you configure a bank routing number and account number under Settings → Check Printing, a MICR-style line is printed at the bottom of the check area (use a MICR-compatible font when printing for machine-readable bank scanning). Schema v56 adds check_stock_layout, check_micr_routing, check_micr_account, and check_next_number to companies, and check_number + check_printed_at to bill_payments. Feature flag check_printing (Vendors group, default on). Settings page at /settings/check-printing. 11 new Jest tests cover the migration, settings GET/PUT, layout validation, viewer permission denial, check number allocation and sequential increment, non-check payment getting null check_number, PDF response content-type, and cross-company isolation.

Merge Duplicate Records ✅

v3.43.0 closes the most common pain point left by the new QBO / Xero importer — two slightly-different rows that should be one. New admin tool at /admin/merge (Admin sidebar group, feature flag merge_records, default on) lets admins combine two duplicate vendors, clients, classes, or projects into one. Pick the entity type, choose the source (the duplicate to delete), choose the target (the keeper), click Preview Impact to see a per-table breakdown of how many rows will be reassigned, then Merge to execute. The merge runs as a single SQLite transaction: every foreign-key reference pointing at the source is rewritten to the target, then the source row is deleted — atomic, all-or-nothing. The implementation uses PRAGMA foreign_key_list introspection over every table in the database, so the tool automatically picks up every FK pointing at vendors(id) / clients(id) / classes(id) / projects(id) — including FKs added by future migrations, with no follow-up code change required. Every reassignment also matches on company_id (when the table has one) for defence-in-depth against cross-tenant contamination. Schema v55 adds merge_log — every merge writes a row capturing the entity type, source/target IDs, a full JSON snapshot of the deleted source row, and a per-table affected-count breakdown; an audit_log row also fires with action merge. Recent merges list on the page surfaces the last 200 with timestamps, who ran them, and total rows touched. 12 new Jest tests cover the migration round-trip, FK introspection (confirming transactions, bills, and vendor_credits all show up in the vendor refs list), the preview endpoint returning correct per-table counts, the execute path reassigning every reference and deleting the source, cross-company source-blocking via 404, viewer-can't-merge via 403, the zero-references happy path (source just gets deleted), the merge log being tenant-scoped on GET, and the audit_log row being written on every merge. Total: 506 tests across 56 suites, all green.

Self-Hosted Error Tracking ✅

v3.42.0 ships in-house frontend JavaScript error capture — no external SaaS, no PII leaving the tenant's box. Schema v54 adds error_reports with a fingerprint-deduped (SHA-1 of message + first non-empty stack frame), per-(company, fingerprint, status) UNIQUE so the same bug from 200 page-views collapses to one row with an occurrence_count that increments and last_seen_at that ticks forward instead of N noisy duplicates. Status flow is open → fixed or open → ignored; resolving a row stamps resolved_by / resolved_at and frees the (company, fingerprint) slot so the same bug recurring after a "fix" creates a fresh open row (the UNIQUE includes status). The client side installs three handlers at boot: window.onerror, unhandledrejection, and a console.error wrapper that captures explicit error logs without recursing (original reference saved before patching). A React <ErrorBoundary> wraps <Routes> and renders a standalone "Something went wrong — we've been notified" fallback (no Layout / auth deps, so it works even if the crash happens inside a Route element). reportError(err, context) builds a payload — message, stack, page (window.location.pathname), user_agent, app_version (from __APP_VERSION__, defined at Vite build time by reading server/package.json), caller context — fingerprints client-side via Web Crypto SHA-1, in-memory-dedupes per (fingerprint, 60s) so a hot loop doesn't hammer the endpoint, then best-effort POSTs to /api/errors. Every call site is wrapped in try/catch so the tracker itself can never throw. The POST endpoint is intentionally unauthenticated — errors from /login, the setup wizard, and the post-token portal pages matter most — but is rate-limited via in-memory token bucket (30 requests per IP per minute, IP from req.ip, falsy IPs key to 'unknown'). When a session is present the row gets company_id / user_id filled; otherwise both are NULL and the open-row lookup uses IS NULL (since SQLite's NULL = ? is NULL, not true). Stack traces over 4 KB are truncated with . Admin queue at /admin/errors (admin-only sidebar link, feature flag errors, default on) — Open / Fixed / Ignored tabs, table of Message / Page / Count / First seen / Last seen / User, click any row for a full-detail modal with the complete stack, context_json, app version, fingerprint, and Mark fixed / Ignore buttons. 17 new Jest tests cover the migration round-trip, public POST with NULL company/user, session POST filling company/user, fingerprint dedupe incrementing occurrence_count without inserting (both with and without a session — the null-company path is the most important since /login errors live there), the resolve-then-recur-creates-new-row path, 4 KB stack truncation, the 31st-in-a-minute rate-limit 429, missing-message validation, admin-only list / detail / resolve, cross-company isolation, and viewer-can't-resolve. Total: 494 tests across 55 suites, all green.

Polish Pack — Onboarding, Empty States, Feedback Widget ✅

v3.41.0 ships three coordinated user-experience upgrades. Onboarding checklist lives at the top of the dashboard for new tenants — six steps (connect a bank account, add a vendor, add a client, send an invoice, configure sales tax, invite a teammate), each with a live "done" check that recomputes against the database on every page load. A progress bar visualizes completion; admins can dismiss the card once the team is up and running, and it auto-hides when every required step is done. Schema v53 adds companies.onboarding_dismissed_at to track the dismiss state. Empty-state pass sweeps every list page (transactions, invoices, bills, estimates, vendors, clients, projects, purchase orders, accounts, anomalies, bank matches, recurring transactions, recurring invoices, time entries) to use the shared <EmptyState> component with a meaningful icon, a one-line explanation of the page's purpose, and a primary CTA that matches the existing Create affordance — so a brand-new tenant always sees "what should I do here?" instead of a blank pane. Feedback widget is a floating brand-pill button mounted bottom-right on every authenticated page. Click it to send a categorized message (bug / suggestion / question / other), an optional 1-5 emoji rating, the page path, and the user agent. Submissions land in a new feedback table and surface in an admin triage queue at /admin/feedback with status filters (open / triaged / closed), per-row detail panel, and a triage form (status + internal notes, stamped with reviewer + timestamp). Optional feedback_submitted notification event fires admin emails on submission. Feature flag feedback (Admin group, default on) gates the widget + page. 10 new Jest tests cover the migration round-trip, the onboarding step computation (brand-new tenant has 5 of 6 undone; inserting an account flips the bank step to done), admin-only dismiss, feedback length validation (rejects empty / oversize), cross-tenant isolation on the admin list, the triage stamping flow, and viewer permission denial. Total: 477 tests across 54 suites, all green.

QBO / Xero Import Wizard ✅

v3.40.0 ships a one-shot onboarding path for businesses migrating from QuickBooks Online or Xero. Schema v52 adds import_runs — every upload is recorded with status (previewcommitted / failed), counts (total / imported / skipped / errored), and the column mapping the user committed against. A new server/lib/importMappers.js heuristically detects QBO vs Xero by inspecting CSV header signatures (Account / Type / Detail Type is QBO Chart of Accounts; Code / Name / Type / Tax Rate is Xero; ContactName / EmailAddress is Xero contacts; Customer / Phone Numbers / Bill To is QBO customers; etc.) across seven supported data types: chart of accounts, customers, vendors, transactions, invoices, bills, and journal entries. When no signature matches, the format falls back to generic_csv and the user hand-maps columns. New server/routes/imports.js mounts at /api/imports and exposes a three-step API: POST /upload parses headers + a 5-row sample preview and stages the raw CSV to server/uploads/imports/<companyId>/<importRunId>.csv; the user reviews the suggested mapping; POST /:id/commit applies the mapping in a single db.transaction() so any thrown error rolls back EVERY row — no partial imports, ever. Failed runs are recorded with the error list but persist zero data. Invoice + bill commits post the standard issuance JE (Dr AR / Cr Revenue, Dr Expense / Cr AP) via postJournalEntry so the General Ledger stays consistent. CoA commits create gl_accounts rows directly. Vendors / customers / transactions commit without JEs. New page at /imports/wizard walks through Upload → Map → Commit with a per-field column dropdown editor, a live sample-row preview, and a recent-imports table that shows status + counts. Feature flag imports_wizard (Sync & Integrations, default on). The existing /import CSV page stays as a sibling for bank-statement-style imports. 23 new Jest tests cover the migration round-trip, format detection across all data-type-and-platform pairs, mapRow for every shape, the upload preview endpoint, successful 3-row commit, vendors-without-JE commit, invoice commit posting AR/Revenue JEs, and most importantly a rollback test that confirms a mid-stream failure persists nothing — the load-bearing guarantee. Total: 467 tests across 53 suites, all green.

Purchase Orders ✅

v3.39.0 closes the AP loop by letting small businesses commit to a vendor purchase before the bill arrives — for tracking outstanding commitments, approval workflows, and the classical "three-way match" (PO → goods received → bill matches PO). Schema v51 adds three tables: purchase_orders (po_number auto-numbered via new companies.po_prefix + po_next_number columns, status flows draft → sent → partially_received → received → billed → closed | cancelled, optional project + class tags, optional linked_bill_id back-reference), purchase_order_line_items (description, qty, unit_price, amount, qty_received running counter, optional inventory_item_id + category_id), and purchase_order_receipts (per-receipt audit trail with line_id + qty + date + notes — multiple partial receipts roll up into the line's qty_received). The bills table gains linked_po_id so converted bills point back at their PO. Routes at /api/purchase-orders: list with filters (status, vendor, project, date range), summary card (open commitment $, open count, draft count), full CRUD, POST /:id/send (draft → sent), POST /:id/receive { line_id, qty, received_date, notes } (writes a receipt row, increments qty_received, recomputes status — all lines full → received, any but not all → partially_received, over-receive is rejected), POST /:id/convert-to-bill (atomically creates a bills row + bill_line_items from the PO using the existing postBillIssueJe so the JE posts normally and respects the bill-approval threshold, sets purchase_orders.linked_bill_id, flips PO status to billed), POST /:id/cancel (blocked once billed), DELETE /:id (admin only, draft or cancelled only), and GET /:id/pdf (PDF rendered via loadBranding for logo / primary color / footer — matches the invoice/report look). Three-way match is wired into the bill-approval response: when bill.linked_po_id is set, the approve endpoint compares bill total ↔ PO total (advisory warning above 1% drift) and bill line-count ↔ PO line-count (advisory warning if they differ) — admin can still approve, the warnings ride along on the response so the UI can surface them. Feature flag purchase_orders (Vendors group, default on). New page at /purchase-orders with summary cards, status filter, per-row Send / Receive / Convert-to-Bill / PDF / Edit / Cancel / Delete actions, a full-detail modal with receipt history, an inline receive modal that only shows lines with remaining qty, and a Convert-to-Bill flow that routes to /bills post-convert. 8 new Jest tests cover the migration round-trip, auto-numbered PO creation + total recompute, PUT-blocked-when-billed, partial → full receipt status transitions + over-receive rejection, convert-to-bill creating a balanced JE-posted bill with the PO linked both ways, convert blocked on cancelled / already-billed, delete restricted to draft/cancelled, and the three-way-match warning surfacing on bill approval when PO ↔ bill totals diverge >1%. Total: 444 tests across 52 suites, all green.

Granular Roles ✅

v3.38.0 expands the user-role model from a binary admin/viewer split into five roles that match how real small-business teams actually divide work. Admin keeps full access (everything, including platform-admin features and company-level settings). Bookkeeper is the new "day-to-day" role — they can create/edit transactions, invoices, bills, run bank reconciliations, manage clients/vendors/projects, and post manual journal entries; they cannot manage users, change company settings, or touch period locks. Accountant is read-only across all data, plus the ability to download PDF and CSV exports of every standard report (P&L, Balance Sheet, Trial Balance, Cash Flow, AR/AP aging, P&L Comparison, expenses-by-category, revenue-by-client) — the role we'd hand the outside CPA at year-end. Approver can only see and act on the bill approval queue — handy when a manager signs off on AP but doesn't enter data. Viewer stays as read-only with no export rights — fine for a stakeholder who needs the dashboard but shouldn't be able to walk away with the books. Schema v50 rebuilds the users table to extend the role CHECK constraint to the new five values (existing admin/viewer rows are preserved verbatim). The middleware layer adds a generic requireRole(...) factory plus three convenience shortcuts (requireWriteAccess, requireApproverAccess, requireReadAndExport) — admin always satisfies any role via an explicit override. Every write route in transactions, invoices, bills, estimates, customerPayments, clients, vendors, accounts, transfers, projects, recurring, import, and journalApi now gates on requireWriteAccess (admin + bookkeeper). Bill approve/reject moved to requireApproverAccess (admin + approver). The nine PDF/CSV export endpoints in reports.js moved to requireReadAndExport (admin + bookkeeper + accountant). Admin-only routes (users, credentials, period lock, feature flags, settings PUTs, category catalog) are untouched. Admin → Users role picker now offers all five roles with inline descriptions. 9 new Jest tests cover the migration, each helper's allow/deny matrix, the admin override, and the live "bookkeeper posts a transaction; accountant cannot" / "approver approves a bill; bookkeeper cannot" / "accountant downloads a P&L PDF; viewer cannot" round-trips. Total test count: 436 across 51 suites, all green.

AI Anomaly Detection ✅

v3.37.0 ships an always-on review queue for things that look wrong on the books. Five detectors feed one unified anomalies table (schema v49). Four are deterministic — no LLM cost, run nightly via cron at 4 AM — and one is opt-in LLM-assisted, only invoked when an admin clicks "Deep Scan (AI)". Detectors: Duplicate Payments finds expense transactions OR bill payments to the same vendor for the same amount within ±3 days, flagged severity high (catches the classic "vendor sent two invoices, both got paid"). Expense Spikes computes a trailing-90-day mean and standard deviation per category, then flags any transaction more than 2σ above the mean (filtered to categories with mean > $50 and stdev > 0 to suppress noise — severity medium). Missing Receipts flags expenses over $250 with no attached receipt and date within the last 30 days, for audit prep — severity low. Uncategorized flags any expense older than 30 days that's still missing a category — severity low, a cleanup nudge. AI Outlier (Deep Scan) pulls the last 60 days of transactions, sends them to Claude with a report_outliers tool, and asks for up to 10 things that look unusual relative to history (new vendor with an oddly-large charge, round-number amounts that look like placeholders, categories that didn't exist before, etc.) — severity comes from the LLM. The AI scan is gracefully skipped when ANTHROPIC_API_KEY isn't configured and respects the per-company daily token budget. Every detector is idempotent thanks to a UNIQUE(company_id, kind, primary_entity_type, primary_entity_id, related_entity_id) constraint — re-running won't create duplicates, and previously-dismissed anomalies are never re-opened. Dashboard surfaces a one-line banner when open anomalies exist (clicks through to /anomalies). The Anomalies page has tab filters (All / Duplicates / Spikes / Missing Receipts / Uncategorized / AI Outliers), status filters (Open / Dismissed / Resolved / All), per-row View / Resolve / Dismiss actions, and Run Scan + Deep Scan buttons. Routes at /api/anomalies (list, scan, dismiss, resolve). Feature flag anomalies (AI & Automation, default on). 8 new Jest tests cover the migration, each deterministic detector independently, idempotency, dismiss-prevents-re-detection, and graceful skip on missing API key.

Quarterly Estimated Tax Calculator ✅

v3.36.0 answers the most common question a small-business owner asks every quarter — "how much do I owe the IRS and state right now?" — without leaving the books. A new Estimated Tax page (Payroll & Tax group) pulls year-to-date net income from the GL through the selected quarter end, applies the company's tax profile (filing status, federal effective rate, SE tax, QBI %, state code + rate, prior-year total tax for safe harbor, and any YTD withholding), and surfaces four big numbers at the top: YTD Net Income, Estimated Federal Due, Estimated State Due, and the IRS deadline (Q1 4/15, Q2 6/15, Q3 9/15, Q4 1/15). A full calculation breakdown is shown right below so the owner can audit every step — revenue, expenses, QBI deduction, SE tax on 92.35% of SE earnings, the ½ SE deduction, federal income tax on taxable income, state tax on the QBI-adjusted base, and any apportioned withholding. Pass-through entities (sole prop / SMLLC / S-corp / partnership) get the QBI deduction; C-corps skip QBI and SE entirely. When prior-year total tax is populated, the 110% safe harbor number is computed and surfaced as the suggested payment whenever it's lower than the year-to-date calculation — paying that amount protects from the underpayment penalty even if this year's actuals come in higher. Recording a payment is one click: pick jurisdiction (federal or state), quarter, amount, date, and (optionally) a bank account. If an account is picked, the app posts a real expense transaction with a balanced Dr Expense / Cr Bank journal entry — wired through the same postJournalEntry pipeline as every other money movement — so the payment flows through to the P&L and cash flow statements automatically. Deleting a payment reverses the JE and removes the linked transaction in one shot. Schema v48 adds tax_settings (one row per company) and estimated_tax_payments. Settings UI lives at /settings/tax-settings. 10 new Jest tests cover the migration, settings GET/PUT/validation, the $40k sole-prop arithmetic, the C-corp short-circuit, the withholding-reduces-due path, the safe-harbor surfacing, the prior-quarter-payments deduction, and the payment-creates-transaction + JE-reversal round-trip. Important: this is a planning tool, not a substitute for a CPA. It uses simplified effective-rate math — no progressive brackets, no AMT, no NIIT, no household itemized deductions. The page carries a disclaimer banner saying exactly that.

Email-to-Bill ✅

v3.35.0 lets vendors mail bills straight into your AP queue. Two ingest paths share a single Claude-backed extractor: an Import Bill drag-drop modal on the Bills page (PDF/PNG/JPG, ≤10 MB) and a per-company inbound email address like bills+<token>@inbox.geekonomics-app.com. The extractor pulls vendor, bill number, dates, total, tax, and line items, matches the vendor by case-insensitive name (creating it if new), and drops the bill into the existing Pending Approval queue — no JE posts until a human admin clicks Approve. Token usage counts against the daily AI budget. Migration v47 adds companies.email_inbound_token (UNIQUE, generated on first request, admin-rotatable) and an email_inbound_log audit table that records every inbound message with its status (processed / failed / rejected), the bill it produced (if any), and trimmed raw metadata. The /api/inbound/email webhook accepts both SendGrid Inbound Parse-style multipart and Mailgun/Postmark-style JSON; it always returns 200 so providers don't retry-storm. Operational notes: point an MX record for inbox.geekonomics-app.com (or your own subdomain — override with INBOUND_EMAIL_DOMAIN) at your inbound-email provider, then route the catch-all (*@inbox.geekonomics-app.com) to https://<your-host>/api/inbound/email. Optionally set INBOUND_EMAIL_SECRET and append ?secret=… to the webhook URL. 10 new tests cover the migration, both ingest paths, vendor matching, budget exhaustion, and unknown-token rejection.

Playwright End-to-End Test Suite ✅

v3.34.0 adds a real-browser regression net on top of the existing 399-test Jest unit suite. Specs live in client/e2e/ and run via npm run e2e (headless) or npm run e2e:ui (interactive). A globalSetup hook builds the client with vite build, copies the bundle into server/public/, spawns the Node API on port 3101 with NODE_ENV=production, DB_PATH pointed at a fresh tempdir SQLite file, and the encryption + session secrets pinned to throw-away values. It polls a new /api/healthz endpoint until the server is up, registers an e2e@example.com admin via /api/register, then runs seedFullDemo.js --company-id=1 --yes against the same DB so every page has realistic data to render. One process serves both the API and the built SPA — no separate Vite dev server. globalTeardown SIGTERMs the server and rm -rfs the temp dir. Seven spec files cover auth (login + already-signed-in banner), dashboard StatCards, transactions list + filtering, invoices list + create modal, bills list, reports (P&L default view, Cash ↔ Accrual basis toggle, Statement of Cash Flows page), and admin Settings (Invoice Branding section). server/db.js was extended to honor a DB_PATH env var so tests never collide with server/data/geekonomics.db. One-time install per machine: npx playwright install chromium. client/e2e/README.md documents the bootstrap flow end-to-end.

Per-Company PDF Branding ✅

v3.33.0 lets each tenant make customer-facing PDFs look like their own. Upload a logo (PNG/JPG/SVG, ≤2 MB) from Settings → Invoice Branding; pick a primary brand color with a native color picker (or paste a hex like #FF8800); set default footer text, payment instructions, and terms. The logo lands at top-left of every invoice PDF (60×60 fit, gracefully skipped if the file is corrupt). The primary color drives the invoice INVOICE label, the line-items table header band, the TOTAL bar, and the accent strip on every report PDF (P&L, Balance Sheet, Trial Balance, Cash Flow, AR Aging, Expense Breakdown, Revenue by Client, P&L Comparison). Payment instructions and default terms print below the line items on each invoice; footer text prints centered at the bottom of every page. Migration v46 adds the five new companies columns; a Preview button on the settings page opens the latest invoice's PDF in a new tab so admins can see their changes immediately. 11 unit tests cover migration round-trip, loadBranding defaults, color regex validation, 2 MB upload limit, prior-logo deletion on re-upload, DELETE endpoint, and end-to-end PDF rendering with + without branding.

Bank Reconciliation Auto-Match ✅

v3.32.0 ends the duplicate-row problem that comes with bank feeds. When new rows arrive from Plaid sync or CSV import, the matcher scans manual transactions, transfers, bill payments, and customer payments for same-date + same-amount (±$0.01, ±3 days) candidates and writes them to a new bank_matches table at status suggested. The new Bank Matches page (/bank-matches, under the Money group) groups suggestions per bank row with a one-click Confirm or Dismiss for each candidate. Confirming flags the bank row with a MATCHED to X note and links it via new matched_to_type / matched_to_id columns, reverses any journal entry that bank row had posted, and auto-dismisses the remaining suggestions for the same bank row so it disappears from the queue. Migration v45 ships the table + columns, extends the transactions.source CHECK to include 'import', and backfills source='plaid' on any older Plaid-synced rows so the matcher works on production data. Plaid sync now tags rows with source='plaid', CSV import uses source='import', and both call proposeMatches after each ingest batch. Behind a bank_matches feature flag (default on). 8 unit tests cover migration round-trip, scoring curve (0d→1.0, 1d→0.9, 2d→0.8, 3d→0.7), the amount / date thresholds, dismissal idempotency, JE reversal, and candidate variety across all four record types.

1099-MISC Support ✅

v3.31.0 adds full 1099-MISC generation alongside the existing 1099-NEC. Vendors now declare which form they receive (form_1099_type: NEC or MISC) and — for MISC — which box their payments belong in (1 Rents, 2 Royalties, 3 Other Income, 6 Medical & Health, 10 Attorney Proceeds). Migration v44 adds the two new vendor columns and rebuilds form_1099_filings to track NEC and MISC filings separately for the same vendor + year. Aggregation applies the box-specific IRS threshold ($10 for royalties, $600 for the other boxes) and the PDF route renders a dedicated MISC layout with the correct box label. The Forms 1099 page now has a NEC/MISC tab toggle, a Box column for MISC, and an inline Edit button to fix a vendor's form type / box without leaving the screen. Vendor edit form gains the same two controls.

Cash Basis P&L Toggle ✅

v3.30.0 adds an Accrual / Cash toggle on the P&L report. Cash basis ignores open invoices and bills until they're paid; revenue is recognized when payment is received and apportioned across the original invoice's revenue distribution, expense is recognized when bills are paid and apportioned across the original bill's expense distribution. Direct cash transactions (Stripe/Square/Plaid syncs and manual bank entries) flow into both views identically. Includes per-revenue-account and per-expense-account breakdowns plus net income, with matching PDF and CSV exports. New helper server/lib/cashBasisPnl.js exposes computeCashBasisPnl(db, companyId, start, end). 8 unit tests cover apportionment, partial payments, period boundaries, multi-line revenue distribution, and the open-invoice-excluded case.

Classes Replace Business Lines + Accordion Sidebar ✅

v3.29.0 folds two overlapping tagging dimensions (business lines and classes) into one. The conceptual story: a single legal entity that runs multiple operations needs one segment dimension, not two. Classes wins because it already supports nesting (parent_id) and is the more general primitive.

Migration v43 mirrors every existing business_line into a class of the same name (carrying over is_pre_revenue and color), adds class_id columns to the seven tables that didn't have one (estimates, clients, projects, recurring_transactions, recurring_invoices, time_entries, memorized_journal_entry_lines), and back-fills class_id from business_line_id everywhere both columns coexist. The legacy business_lines table and business_line_id columns stay in place dormant for one release as a safety net.

Server: every route now writes class_id only; the legacy business_line_id body field is still accepted as an alias and resolved via name-match to the corresponding class. The /api/business-lines POST/PUT routes now mutate the classes table under the hood. The by_business_line block was removed from /api/reports/pnl/api/reports/pnl-by-class is the canonical segment breakdown.

Client: every page that fetched /api/business-lines now fetches /api/classes and renames the local state to classes. The duplicate business-line picker on TransactionForm/InvoiceForm/BillForm is gone — one Class picker remains. Setup wizard step 3 is now "Classes". Reports page links out to /reports/pnl-by-class instead of rendering a now-empty bar chart.

Accordion sidebar (v3.28.1) also rolled into this release: the sidebar opens one group at a time, auto-expands the group of your active route, persists in localStorage, and shows item counts + an active-group dot per header.

Feature Flags + Categorized Sidebar ✅

v3.28.0 ships two things together:

Categorized sidebar. The flat 30+ link sidebar is now organized into collapsible groups: Overview, Money, Customers, Vendors, Operations, Payroll & Tax, Reports, AI & Automation, Sync & Import, Admin. Each group can be collapsed to clean up the nav for the company's actual workflow. Org Admin + Platform Admin links float to the bottom for admin users.

Per-company feature flags. Settings → Features lets an admin turn individual features on/off. Disabled features are hidden from the sidebar; the flag persists per-company in companies.feature_flags JSON. Core features (Dashboard, Transactions, Accounts, Reports, Settings, GL) are locked-on. Each feature has a description so the admin knows what they're toggling.

Also: P&L by Class report at /reports/pnl-by-class, slicing revenue/expense/net by the classes tagged on journal_lines (paired with v3.12.0).

Schema v42. Tests cover features endpoint, lock semantics, default-on, and the helper.

Statement of Cash Flows ✅

v3.27.0 adds /reports/cash-flow-statement, an indirect-method cash flow statement. Starts from Net Income for the period, adds back non-cash depreciation, adjusts for changes in working-capital accounts (AR, AP, inventory, prepaid), then reports Investing Activities (Fixed Assets change) and Financing Activities (owner equity contributions + draws). Computed net change in cash is reconciled against the actual change in bank-account balances; any discrepancy is surfaced as an alert so you can spot misclassifications. Configurable date range. Companion to the P&L and Balance Sheet — completes the three core financial statements.

Receipts Library ✅

v3.26.0 ships a /receipts page that surfaces every expense transaction with a receipt attached, in one place. Filter to "missing receipts" to see expenses lacking documentation. Coverage stat shows the % of expenses with receipts on file (audit-prep friendly). Date-range filtering. Each row deep-links to the underlying receipt file (image or PDF).

Client Portal ✅

v3.25.0 mirrors the vendor portal for customers. Admin generates a 90-day portal link from a client row (auto-copied to clipboard); send to client. They open /client/<token> to see outstanding balance, lifetime paid, all their invoices with status, and full payment history (with the applied-to invoice numbers). Schema v41 adds client_portal_tokens. Tests cover issuance, public view with payment-application mapping, and revoke.

Vendor Portal ✅

v3.24.0 adds vendor-facing self-service. Admin generates a portal token from the Vendors page (link gets auto-copied to clipboard); send to vendor. Vendor opens /vendor/<token> to see their bills (with status, due date, total, paid, outstanding), payment history, and an outstanding-balance summary card. Default expiry is 90 days; admin can revoke from the vendor's portal token list anytime. No login required — token = access. Schema v40 adds vendor_portal_tokens. Tests cover issuance, public access, revocation, and bad-token 404.

Event Notifications ✅

v3.23.0 adds opt-in event notifications. Admin chooses which events to subscribe to (invoice_paid, bill_overdue, low_stock, period_locked, large_je, bill_awaiting_approval) and a recipient list (or fall back to all admins). When a wired event fires, an email goes out via the existing per-company SMTP. Wired so far: invoice_paid (manual mark-paid path) and bill_awaiting_approval (created bill exceeds approval threshold). The helper lib/notifications.js is a one-call drop-in so additional events can be wired in incrementally. Schema v39. Tests cover sending, event-gating, and admin-fallback recipient lookup.

Subscription / MRR Report ✅

v3.22.0 adds /reports/mrr — a subscription-style report computed from active recurring invoice templates. Each template's subtotal is normalized to a monthly equivalent (weekly = ÷4.333, biweekly = ÷2.167, monthly = ×1, quarterly = ÷3, annually = ÷12) and summed for MRR (with ARR = MRR × 12). Breakdown by frequency and by client. Lists all templates with per-cycle vs monthly value, status, next-issue date. Recent invoices issued in the last 90 days for activity context.

API Tokens ✅

v3.21.0 adds Bearer-token authentication for programmatic API access. Admins generate scoped tokens in Settings → API Tokens (viewer or admin role, optional expiry). Tokens are shown once at creation and stored only as a bcrypt hash thereafter — the page surfaces only the 10-char prefix going forward. Send as Authorization: Bearer gko_<...> on any API request; the middleware looks up the token, stamps session context, and updates last_used_at. Revoke disables it immediately. Schema v38. Tests cover create, bearer authentication, invalid-token 401, revocation, and expiry.

Sales Tax Liability Report ✅

v3.20.0 adds /reports/tax-liability. Single-page snapshot of what's currently in Sales Tax Payable (read straight from journal_lines), tax collected per rate over a date range (with jurisdiction + invoice count + taxable subtotal), recent invoice-level collection history, and recent remittance history. Used to file-and-pay sales tax with confidence: see the open liability, see what was collected at which rate, then go to /sales-tax to record the remittance JE.

Time Tracking → Invoice ✅

v3.19.0 adds a /time page where staff can log hours against a client, project, and business line, with an optional hourly rate and a billable flag. Summary cards show unbilled hours, unbilled value, billed hours, and total entries. Multi-select unbilled entries → "Create Invoice" rolls them up: groups by description (or as a single "Professional services" line) and creates a draft invoice in one click. Entries become locked to that invoice — editing or deleting a billed entry is refused. Schema v37 adds the time_entries table with FK links to invoice + invoice_line_item. Tests cover logging, summary aggregation, grouping behavior on create-invoice, and lock-after-billing.

Inventory — Sales + Purchase Loop ✅

v3.18.0 wires inventory items into invoices and bills. Schema v36 adds inventory_item_id to invoice_line_items and bill_line_items. Purchase path: when a bill line is tagged to an inventory item, the issuance JE Dr's Inventory Asset (1300) instead of the expense category, and qty_on_hand increments by the line quantity. Sale path: when an invoice line is tagged to an inventory item, the standard Dr AR / Cr Revenue is supplemented by an automatic Dr COGS (5000) / Cr Inventory (1300) at the item's cost_price × qty, and qty_on_hand decrements. Status changes (draft→sent→cancelled), edits, and deletes correctly reverse inventory movements alongside their JE reversals. Trial balance closes to the penny in every scenario. Tests cover purchase, sale, cancel-after-issue (stock returns), and bill cancellation.

Inventory (Foundation) ✅

v3.17.0 ships an inventory module. New /inventory page lists products and services with SKU, qty on hand, cost, sales price, reorder point, and an active toggle. Stock adjustments post a balanced JE: positive change → Dr Inventory Asset (1300) / Cr Other Income; negative change → Dr COGS (5000) / Cr Inventory Asset. Reasons (shrinkage, damage, count adjustment, receipt, return) are tracked, and deleting an adjustment reverses both the qty change and the JE. Creating a new item with opening qty + cost posts an opening-balance JE (Dr Inventory / Cr Owner's Equity). Low-stock items (qty ≤ reorder point) are surfaced on a dashboard card. New seeded GL account: 1300 Inventory Asset. Schema v35 also extends journal_entries.source_type CHECK to allow inventory_adjustment. Tests cover create, multi-adjustment, history fetch, qty-reversal-on-delete, FK-protection on items with history, and low-stock counting.

Custom Fields ✅

v3.16.0 ships user-defined custom fields on transactions, clients, vendors, invoices, and bills. Admin defines fields at /settings/custom-fields: pick the entity, give it a label, pick a type (text / number / date / yes-no / dropdown), optionally mark required and add help text. Defined fields auto-render on the relevant create/edit forms (Transactions wired in this push; the same component plugs into any form via two lines of code). Values are stored typed (text/number/date/boolean) and tied to the def via a FK with ON DELETE CASCADE, so deleting a def cleans up stored values. Schema v34. Tests cover auto-slug keys, typed value storage, clear-by-empty-string, FK cascade on def delete, and the duplicate-key 409.

Two-Factor Authentication (TOTP) ✅

v3.15.0 adds optional 2FA. Settings → Two-Factor Authentication walks the user through scanning a QR code (Google Authenticator, 1Password, Authy, etc.) and verifying a 6-digit code. Secret is stored encrypted at rest using the existing AES-256-GCM credential encryption. 10 single-use backup codes are generated at setup and shown once (hashed at rest). When 2FA is on, login becomes two-step: password verifies → server returns { totp_required: true } and holds a pending-user marker on the session for 5 minutes → client posts the 6-digit code (or a backup code) to /auth/verify-totp to finalize. Disable requires the user's current password. Backup codes are regenerable (also password-gated). Schema v33. Tests cover setup → enable → re-login → backup-code-one-shot → disable.

Bill Approval Workflow ✅

v3.14.0 adds threshold-based bill approval. Enable it in Settings → Bill Approval Workflow and set a dollar amount (default $1,000). When any new bill exceeds that threshold, the bill is created in approval_status='pending' and the issuance JE does not post until an admin approves it. Bill payments are blocked while pending. Admin clicks Approve to post the JE (now stamped with approved_by + approved_at) or Reject (sets status to cancelled and records rejection_reason). Non-admin users cannot approve/reject. Below-threshold bills continue to post immediately as before. Bills page surfaces a "Pending Approval" summary card + inline action icons on each pending row. Schema v32. Tests cover small/large bills, approve/reject, payment-block, role-gate, and disable-feature paths.

W-9 Collection Workflow ✅

v3.13.0 ships a tax-season essential. New admin page at /w9 lists every vendor (1099-flagged ones surfaced first) with their W-9 collection status. Click "Create" to generate a unique-token request, then "Send" — vendor receives an email with a secure link to a public self-service form (/w9/<token>). They fill out legal name, business name, tax classification, EIN/SSN, address, and a typed signature. On submit, the W-9 record is stored and their TIN + city/state/zip get pushed back to the vendor record, so 1099 generation auto-picks them up. Public link expires after 30 days; resubmission is refused. Admin can also "Mark received" (for paper W-9s) or "Copy link" (to share via another channel). Schema v31. Tests cover create / send / public submit / re-submit refusal / vendor backfill.

Classes / Department Tracking ✅

v3.12.0 adds a second tagging dimension alongside business lines. New classes table (id, name, code, parent_id, is_active) — supports nested classes/departments. Migration v30 adds class_id to transactions, transaction_splits, invoices, bills, and journal_lines. Every operation that posts a JE now threads the class_id through every line so reports can slice by class. Admin CRUD page at /classes. Class picker shows up on TransactionForm / InvoiceForm / BillForm when at least one class exists, and a Class filter appears on each list page. Delete is refused while a class is in use anywhere — set is_active=false to retire one instead.

Mass Categorize Tool ✅

v3.11.0 ships a power-user /mass-categorize page. Punch in a pattern (e.g. UBER, AMZN MKTPLACE), choose contains / starts-with / exact, optionally filter to uncategorized-only, hit Preview — page lists every matching transaction. Pick a category, vendor, business line, and/or project, then apply to the whole set in one click. Optional "Save as auto-categorize rule" checkbox creates a categorization_rules entry at the same time, so future imports auto-classify. /api/transactions/bulk-categorize now also accepts project_id so the project tag is part of the bulk update.

Per-Project P&L Report ✅

v3.10.0 promotes the per-project P&L modal from v3.7.0 into a standalone report at /reports/project-pnl. Single-page roll-up of every project: revenue, expense, net, budget, and budget-utilization % — pulled directly from journal_lines so the numbers tie to the GL. Date range + status filters. Totals row at the bottom. Color cues when utilization crosses 90% (amber) or 100% (red).

Burn Rate & Runway Report ✅

v3.9.0 ships a Burn Rate & Runway report at /burn-rate. Pulls income and expense by month directly from journal_lines for the trailing 6, 12, or 24 months, computes 3-/6-/12-month average net cash flow, and divides current cash by burn to project runway in months. When the business is profitable (positive average net) the runway is reported as ∞ — the metric only makes sense when net is negative. The page shows summary cards, a monthly income / expense / net bar chart, the bank account balances making up current cash, and the top expense drivers from the last 3 months. Triggers a warning banner below 6 months of runway and a stronger one below 3.

Demo Dataset Seeder + Fresh-Install Migration Fix ✅

v3.8.0 ships server/scripts/seedFullDemo.js, a safe + idempotent script that wipes a tenant's data and replaces it with a realistic 12-month dataset spanning every shipped feature: 5 bank accounts, 3 business lines, 8 clients, 10 vendors, 4 projects, ~160 transactions (with one multi-line split + a handful of intentionally-uncategorized rows for AI/rules testing), 8 invoices in mixed statuses with applied customer payments, 8 bills with mixed statuses and payments, 3 transfers, 3 estimates, a customer credit memo, a vendor credit, recurring transactions, a memorized JE, a fixed asset with one depreciation entry, and an annual budget. Posts balanced journal entries for every operation so the trial balance closes to the penny. Refuses to run with NODE_ENV=production, defaults to dry-run unless --yes, requires --company-id=N, and supports DB_PATH= for testing against a copy. Also adds migration v29 — a forward-fix for fresh-install DBs whose journal_entries.source_type CHECK was missing the credit_memo / vendor_credit values added in v3.5.0, which would have silently blocked those JE source types on any tenant first installed on v3.5.0+. (Pre-existing tenants upgraded through v26 were unaffected.)

Project / Job Costing — UI Tagging ✅

v3.7.1 wires the project picker into the Transaction, Invoice, and Bill forms so day-to-day data entry can tag work to a project. Each of those list pages also gains a Project filter and shows the project label inline on the row. Backend list endpoints (/api/transactions, /api/invoices, /api/bills) accept ?project_id= and now return project_name + project_code via LEFT JOIN to projects.

Project / Job Costing ✅

Track income and expense per project. New /projects page with status (active/on_hold/completed/canceled), optional budget, optional client + business-line linkage, start/end dates. Transactions, invoices, and bills can be tagged to a project (backend) and every resulting JE line carries the project_id so reports can drill down. The per-project P&L (GET /api/projects/:id/pnl) reads directly from journal_lines filtered by project, so it's accurate against the GL — not a separate derived total. UI shows per-row aggregate revenue/expense plus a P&L modal with optional date filter and a budget-vs-actual progress bar. Shipped as v3.7.0.

Memorized Journal Entries ✅

Save any balanced journal entry as a reusable template. Mark it manual or set a frequency (weekly/biweekly/monthly/quarterly/annually) and it auto-posts via the daily cron. Perfect for monthly prepaid amortization, accrual reversals, depreciation top-ups, or any recurring adjusting entry. Each line is tag-able to a business line, client, or vendor. One-click "Post Now" button for off-schedule posting. Shipped as v3.6.0.

Customer Refunds & Vendor Credits ✅

First-class credit memo and refund workflows for both customers and vendors (v3.5.0). Issue a customer refund (cash back to a bank account) or a customer credit (apply to a future invoice). Same for vendor sides — record a vendor refund or vendor credit and apply it to an open bill. Every action posts a balanced JE that integrates with the GL: customer refunds Dr Revenue / Cr Bank, customer credits Dr Revenue / Cr Customer Deposits, application Dr Customer Deposits / Cr AR. Vendor refund Dr Bank / Cr Expense, credit Dr Vendor Credits Held / Cr Expense, application Dr AP / Cr Vendor Credits Held. New system account "Vendor Credits Held" (1450) is auto-seeded. Bill and invoice status recompute now factor credit applications, so a fully-credit-applied invoice automatically shows as paid.

Comparative P&L Report ✅

The P&L Comparison report now supports three modes: Prior Year (YoY, default), Prior Period (same-length immediately preceding window), or No Comparison. Results break down by GL account (revenue + expense, each with current/prior/Δ$/Δ%) alongside the existing business-line view. The "favorable variance" coloring flips for expenses (down is good) vs. revenue (up is good). Shipped as v3.4.0.

Bookkeeper Productivity Pack ✅

Three accountant/bookkeeper productivity features in one push (v3.3.0) targeting day-to-day workflow speed.

  • Multi-line Transaction Splits — a single transaction can now be split across multiple categories with one balanced journal entry. The "Home Depot $400 = $300 supplies + $100 tools" scenario finally works correctly. The transaction form has a "Split into multiple categories" toggle that expands a per-line editor with live "Remaining: $X" feedback and the GL posts one bank line + N category lines, with each split-line tag-able to its own business line, client, or note.
  • Bulk Transaction Edit — multi-select on the Transactions page lets you assign category, vendor, and business line to dozens of imported transactions at once; the GL is correctly re-posted for every row whose category changed (split transactions are left alone since they're already detailed).
  • Audit Trail Page — admins now have a searchable /audit-trail view of every action (logins, settings changes, sync runs, deletes, reversals, etc.) with filters by action, entity, user, and date range, plus inline JSON details for each entry. Color-coded action badges, paginated, ready for SOC-style review.

Invoicing Suite — Online Pay, Recurring, Dunning, Forecast ✅

Four customer-experience and reporting features in one push (v3.2.0) that close the biggest workflow gaps after Tier 2.

  • Online Invoice Payment — every open invoice gets a "Copy online pay link" action. Customer hits /pay/<token> and is redirected to Stripe Checkout. Stripe's webhook auto-records the payment + posts the Dr Stripe Clearing / Cr AR JE; the invoice flips to paid without any manual data entry. Per-company webhook signing secret + Stripe API key. Idempotent against duplicate event delivery.
  • Recurring Invoices — new /recurring-invoices page (admin) with a full template editor: client, frequency (weekly/biweekly/monthly/quarterly/annually), next-issue date, line items, tax rate, due-date offset, notes, terms. Optional auto-charge ticks Stripe off-session against the client's stored payment method on each issuance. Daily cron at 1 AM issues all due templates across all companies. Manual "Issue now" + pause/resume per template.
  • Customer Statements & Automated Reminders — new /automated-reminders page. Per-company 3-stage escalating cadence (defaults: 7 / 14 / 30 days overdue) with subject + body templates and tokens like {client_name}, {amount_due}, {pay_url}. Daily cron at 9 AM. Per-stage dunning log so reminders never duplicate. Plus an on-demand "Statement of Account" email per client.
  • Cash Flow Forecast — new /cash-flow-forecast report. 30/60/90/180-day projection blending starting bank balances with open invoices, open bills, recurring transactions, and recurring invoice issuances + their predicted payment dates. Surfaces low points and alerts when projected cash dips significantly or goes negative.

AI Auto-Bookkeeper ✅

The AI Assistant graduated from advice-only to action-taking. A new /ai-bookkeeper page lets you run Claude across four review jobs against your books and approve the proposals before anything posts. Every approval routes through the normal posting paths, so period locks, GL invariants, and multi-tenant scoping all still apply.

  • Categorize — Claude proposes category + vendor for uncategorized transactions, citing the description text
  • Match transfers — deterministic scan finds same-day same-amount income/expense pairs across two accounts, proposes deleting both transactions and creating one transfer in their place
  • Audit categories — Claude reviews recent categorized transactions and flags clear mismatches (only fires at ≥75% confidence)
  • Suggest journal entries — Claude proposes balanced adjusting entries (accruals, prepaid amortizations, accrued payroll); unbalanced or invalid-account proposals are rejected before they reach the review queue
  • Run Everything mode runs all four jobs in sequence and aggregates the proposals into one run
  • Each proposal carries a confidence score (0–100%), a one-sentence reason, and a kind badge; admins can approve/reject per-row or bulk-approve a whole run
  • New ai_runs + ai_proposals tables (migration v23); every Claude call is logged with token usage and model for cost/quality tracking
  • Prompt caching on the system prompt keeps repeat runs cheap; defaults to Claude Sonnet 4.6

Chart of Accounts + Double-Entry General Ledger ✅

The accounting foundation. Geekonomics now keeps a real chart of accounts and posts balanced journal entries for every financial movement.

  • 28-account default Chart of Accounts seeded for every company (Assets / Liabilities / Equity / Revenue / Expenses) with system accounts that integration code looks up by subtype
  • gl_accounts, journal_entries, journal_lines tables (migration v14)
  • Every transaction, transfer, invoice issue, invoice payment, and payroll run posts balanced debits/credits to the GL
  • Existing data from prior versions is backfilled on upgrade — opening balances become equity, transactions become Dr Bank / Cr Revenue (or vice versa), invoices become Dr AR / Cr Revenue, payroll splits into wages, withholding liabilities, and net cash
  • Manual journal entries available via API; UI to follow in next release
  • Chart of Accounts management UI in Settings → Chart of Accounts
  • Balance Sheet, Trial Balance, and P&L now read directly from the GL (and stay backward-compatible with existing report consumers)
  • New GL Detail report for drill-down by account with running balance

Multi-tenant Platform ✅

The hosted version at books.geekonomics-app.com supports multiple isolated company tenants on a single install.

  • Sign-up & Registration — create a brand-new company tenant from books.geekonomics-app.com
  • Forgot Password & SMTP — self-service password reset via email; platform-wide or per-tenant SMTP config
  • Admin Console — per-tenant /admin page with Users tab and Audit Log tab
  • Platform Super-Admin — cross-tenant operator panel at /platform (deactivate companies, reset passwords)
  • Audit Log — every significant action recorded per-tenant with user, action, details, timestamp

Payroll ✅

Run payroll with federal and Utah state withholding using current tax tables.

  • Employee records (W-4 fields, exemptions, pay rate, schedule)
  • Federal income tax (Pub 15-T 2026 percentage method) and FICA (Social Security + Medicare)
  • Utah state withholding (2026 tables)
  • Pay run lifecycle: draft → calculated → processed
  • Pay stubs and per-run summaries

Bookkeeping Foundation ✅

  • Transactions (income/expense) tied to bank accounts, categories, vendors, clients, business lines
  • Bank accounts (checking/savings/credit card/cash/clearing) with running balances
  • Inter-account transfers
  • Reconciliation against bank statements
  • Invoices and invoice line items
  • Recurring transactions
  • Auto-categorization rules
  • Receipt attachments
  • Stripe, Square, and Plaid (bank) sync
  • CSV transaction import
  • Reports: P&L, P&L comparison, expenses-by-category, revenue-by-client, AR aging, cash flow, balance sheet, trial balance, Stripe fees
  • AI Assistant powered by Claude

Manual Journal Entries ✅

Full UI for entering balanced journal entries at /journal. Two-pane layout: paginated list of all entries (with source type — manual, transaction, invoice, payroll, etc.) and a detail view showing every line with the account, debit, and credit. Admins can post manual entries via a multi-line form that validates balance in real time.

Period Close & Lock Periods ✅

Lock prior accounting periods so transactions, transfers, bills, invoices, and journal entries can't be edited or backdated into them. Required for clean year-end closes.

  • New /settings/period-close page (admin) shows current lock status and a full history of lock/unlock/extend events
  • Lock through any past date; "extend" pushes the lock further forward; unlock requires a reason and is recorded in the audit log
  • Every mutation route (POST/PUT/DELETE) on financial data checks the date against the lock and returns HTTP 423 with a PERIOD_LOCKED code if it would edit a frozen period
  • Edits to draft invoices remain allowed even if the issue date falls in a locked range — they post no JEs until issued

Bills & Accounts Payable ✅

First-class tracking of money you owe to vendors. Enter a bill with line items mapped to expense categories, then record one or more payments against it from a bank account.

  • New /bills page with summary tiles (Outstanding, Overdue, Open count), status filtering, and per-bill detail with payment history
  • Bill statuses: draft / open / partial / paid / overdue / cancelled — automatic transitions as payments are recorded and as the due date passes
  • Each issued bill posts a balanced JE: Dr Expense / Cr Accounts Payable; each payment posts Dr Accounts Payable / Cr Bank
  • New AP Aging report on the Reports page bucketing bills by days overdue (current, 1–30, 31–60, 61–90, 90+)
  • Daily cron flips open bills to overdue when their due date passes
  • Reversing a bill or a payment cleanly reverses the GL entries so the books always balance

Sales Tax Tracking ✅

Track and remit sales tax by jurisdiction.

  • tax_rates table — per-company list of named rates (e.g. "Utah State 4.85%", "Salt Lake County 1.35%") with jurisdiction label
  • Invoice form picks a configured rate (or enters a custom percentage); selected rate is stored on the invoice
  • Sales Tax Payable already lives on the Chart of Accounts — every taxable invoice posts the tax portion as a credit there
  • New Sales Tax page (/sales-tax) with summary tiles (Taxable Sales / Collected / Remitted / Outstanding) and a per-rate breakdown for any year or quarter
  • Remittance recorder: pick a tax rate (or "combined"), period, paid-from account, amount → posts Dr Sales Tax Payable / Cr Bank
  • Admin can manage rates at /settings/tax-rates

Customer Payments — Apply to Multiple Invoices ✅

Record one customer payment and split it across multiple open invoices.

  • customer_payments + customer_payment_applications tables (migration v18); invoices.amount_paid for partial tracking
  • "Receive Payment" button on the Invoices page opens a modal with all of the client's open invoices and per-invoice apply fields
  • "Auto-apply (oldest first)" button distributes the payment automatically by due date
  • Each invoice application posts its own balanced JE (Dr Bank / Cr AR per invoice) so the GL stays auditable
  • Overpayments land in a new Customer Deposits liability account (2400) and can be applied later
  • Invoice status auto-flips to paid when applications cover the full total

1099-NEC Generation ✅

Track 1099-eligible vendor payments through the year and generate Form 1099-NEC PDFs each January.

  • Vendor records gain a 1099 contractor flag plus TIN/SSN/EIN and full mailing address (migration v19)
  • Company settings gain a TIN/EIN for the payer block
  • New form_1099_filings table records which 1099s have been filed for which vendor in which tax year
  • New 1099-NEC page (/1099) shows per-vendor totals for any year (from bill_payments + transactions), flags vendors missing TIN/address, summary tiles for eligible/total/filed counts
  • $600 IRS reporting threshold enforced; toggle to also view sub-threshold paid vendors
  • Per-vendor PDF working copy of Form 1099-NEC with payer/recipient blocks and Box 1 amount
  • Mark-filed / unmark-filed for record-keeping

Fixed Assets & Depreciation ✅

Track depreciable assets and auto-post monthly depreciation entries.

  • fixed_assets + depreciation_entries tables (migration v20), new Depreciation Expense GL account (6850)
  • Asset register with cost, salvage value, useful life, in-service date, and method (straight-line or double-declining balance)
  • Per-asset Catch-Up button posts all missing months from in-service date through last month-end
  • Monthly cron auto-posts prior-month depreciation on the 1st of every month
  • Dispose Asset records sale proceeds (or scrap), removes the asset and its accumulated depreciation from the books, and posts the gain/loss to Other Income or Other Expense
  • Every depreciation entry posts a balanced JE (Dr Depreciation Expense / Cr Accumulated Depreciation) and is auditable in the GL Detail report

Budgets & Variance Reports ✅

Plan revenue and expenses by account and period, then compare to actuals.

  • budgets + budget_lines tables (migration v21)
  • Period types: monthly, quarterly, or annual — generates the right set of period labels per fiscal year
  • Spreadsheet-style matrix editor: account rows × period columns, with row totals
  • Variance report per period: budgeted vs. actual (computed live from the GL), favorable/unfavorable coloring (revenue ≥ budget = favorable; expense ≤ budget = favorable)
  • Setting an amount to 0 deletes the line so the budget stays clean

Estimates / Quotes ✅

Pre-invoice approval workflow — send a quote, accept it, convert to an invoice.

  • estimates + estimate_line_items tables (migration v22); new estimate_prefix + estimate_next_number on companies (auto-numbering as EST-1001, EST-1002, …)
  • Status flow: draft → sent → accepted / declined / expired
  • One-click Convert to Invoice atomically creates the invoice + copies line items + flips the estimate to converted with a pointer to the new invoice; converted estimates are immutable
  • UI at /estimates with status badges and the right per-row actions for each state

🎉 Tier 2 — Real-World Accounting Features: Complete

All ten Tier 2 features are now shipped:

  1. ✅ Chart of Accounts + GL · 2. ✅ Manual Journal Entries · 3. ✅ Bills & A/P · 4. ✅ Period Close · 5. ✅ Sales Tax · 6. ✅ Customer Payments · 7. ✅ 1099-NEC · 8. ✅ Fixed Assets · 9. ✅ Budgets · 10. ✅ Estimates

Long-Term — Tier 3

Nice-to-have features that round out the package once the foundation is solid. Order is approximate; some may be promoted to Tier 2 based on demand.

FeatureNotes
💡 Time Tracking → BillingLog billable hours per client/project and roll up into invoices
💡 Purchase OrdersPre-bill commitment tracking with three-way match
💡 Multi-currencyForeign currency transactions and FX gain/loss
💡 Customer Statements & Automated RemindersPeriodic AR statements; auto-send dunning emails
💡 InventoryItem master, COGS, reorder points (for product sellers)
💡 Class / Department TrackingA second tagging dimension beyond business_line
💡 Bank Rules Multi-Condition SplitsMulti-condition rules in the auto-categorizer that propose splits (single-txn splits already shipped)
💡 Mobile AppiOS/Android for receipt capture and quick entry
💡 API & WebhooksPublic API for integrations

Customer Experience

FeatureNotes
💡 Customer PortalPer-client login at /portal — view open invoices, statements, payment history, pay online
💡 Estimate E-signatureCustomer signs the estimate in-browser; signature + IP + timestamp captured before convert-to-invoice

AI Extensions

FeatureNotes
💡 AI Anomaly DetectionFlag duplicate payments, unusual expense spikes, vendor charges outside normal ranges
💡 AI Cash Flow Forecast30/60/90-day projection blending invoice due dates, recurring transactions, historical seasonality
💡 Email-to-BillForward a vendor bill PDF to bills@yourbiz.com; Claude extracts amount/date/vendor and creates a draft bill
💡 Natural Language Reports"Show me revenue by client for Q2" generates the report from a plain-English prompt
💡 AI Auto-ReconciliationAfter a bank statement upload, Claude proposes matches between cleared txns and statement lines
💡 Expense Policy EnforcementConfigure rules ("no meal over $80 without note"); Claude flags violators on import

Banking & Payments

FeatureNotes
💡 ACH Vendor PaymentsPay bills directly from a bank account via Plaid or Stripe Treasury — no check printing
Bill Pay Check PrintingGenerate printable checks for bills with vendor address + signature. Shipped in v3.44.0.
💡 Brex / Ramp / Mercury SyncFirst-class integrations for modern business banking (beyond Plaid generic)
💡 Customer ACHAccept ACH-debited customer payments (lower fees than card)
💡 Apple Pay / Google Pay Receipt SyncPull mobile-wallet receipts from email and auto-create transactions

Reporting & Insights

FeatureNotes
💡 Burn Rate & RunwayCritical for pre-revenue business lines — shows months of runway at current burn
Custom Report BuilderPick GL accounts, group by month/quarter/year, run and export to CSV. Shipped in v3.56.0.
💡 Per-Project P&LProfitability by client + project tag (needs project/job costing first)
💡 KPI DashboardDSO, current ratio, gross margin, burn rate widgets configurable per business line

Tax & Compliance

FeatureNotes
💡 Quarterly Estimated Tax CalculatorProject federal + state estimates from YTD P&L; remind before due dates
Schedule C / 1120-S ExportGenerate prep packets for the year mapped to the right form lines. Shipped in v3.45.0.
💡 W-9 Collection WorkflowAuto-email W-9 request to new 1099 vendors; track receipt; flag missing TINs before January
💡 Multi-State PayrollCurrently Utah-only; add California, Texas, New York, Florida tables
💡 Sales Tax Auto-FilingTaxJar or Avalara integration to e-file remittances per jurisdiction

Multi-Entity & Permissions

FeatureNotes
💡 Consolidated FinancialsSingle P&L / Balance Sheet across multiple company tenants with intercompany eliminations
💡 Granular RolesBeyond admin/viewer — add "bookkeeper" (no admin), "accountant" (read-only + report export), "approver" (bill approval only)
💡 Bill Approval WorkflowsConfigurable thresholds: bills over $X require approval from a designated user before posting
Activity per User DashboardWho did what, when — per-user timelines + action breakdown. Shipped in v3.54.0.

Data Migration & Productivity

FeatureNotes
💡 QuickBooks Online ImportOne-click onboarding from a QBO export — bring in COA + history + balances
💡 Xero ImportSame shape for Xero
💡 Mass Categorization ToolFilter txns by description regex → bulk-assign category + vendor
💡 Custom FieldsAdd user-defined fields to transactions, clients, vendors, invoices
Merge Duplicate RecordsMerge two duplicate vendors / clients / classes / projects with FK reassignment. Shipped in v3.43.0.

Integrations

FeatureNotes
💡 ShopifyPull orders, payouts, fees; auto-create invoices per order
💡 Amazon SellerFBA fees, payouts, settlement reports
💡 Gusto / Rippling Payroll BridgeUse their payroll, import the JE into our GL
Slack / DiscordWebhook event notifications to Slack and Discord channels
💡 Chrome ExtensionCapture transactions from a bank web UI when bank lacks Plaid support
💡 Zapier / Make ConnectorTrigger zaps on new invoice, new payment, new bill

Security & Trust

FeatureNotes
💡 2FA / TOTPOptional second factor for all users; required for admins
💡 SAML SSOEnterprise login for hosted multi-tenant version
💡 IP AllowlistPer-company allowlist for admin operations
Session ManagementView active sessions, revoke any you don't recognize. Shipped in v3.55.0.
💡 Encrypted ExportsOptional password protection on CSV/PDF exports

Localization

FeatureNotes
💡 Multi-Language UISpanish first, then French, German
💡 VAT / GST Tax EngineReplace the US-centric sales tax model with a more general indirect-tax engine for non-US users
💡 Localized Number / Date FormatsRespect locale for currency formatting, date order, decimal separator

How this roadmap works

  • Items move from 💡 Idea → 📋 Planned → 🚧 In Progress → ✅ Shipped as design and build proceed
  • Order within a tier is approximate but Tier 1 items build in dependency order (each needs the one above)
  • This page is updated whenever a feature lands or design starts on a new one — check back, or watch the GitHub repo for live commit history

Got a feature request or a vote for what should move up? Open an issue on GitHub.

Geekonomics — bookkeeping for small businesses