Note (2026-04-24): After this document was written, legal_entity was renamed to tenant and the old tenant was renamed to organization. Read references to these terms with the pre-rename meaning.

AR Module Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Add an Accounts Receivable module with an invoice creation/edit UI, overview list, and supporting data model.

Architecture: Split into 7 sequential tasks. First, extract shared PDF renderer and invoice schemas. Then, migrate the database and rename invoiceincoming-invoice across the codebase. Finally, build the AR UI (create/edit form with live PDF preview, overview list).

Tech Stack: Clojure, HTMX, Hiccup, HoneySQL, Malli, OpenHTMLToPDF, PostgreSQL.


Task 1: Extract Shared PDF Renderer

Extract the duplicated OpenHTMLToPDF usage from workers.ap.acquisition.html and integrations.ap.cover-page into a shared protocol.

Files:

Step 1: Write the failing test

Create test/com/getorcha/pdf/renderer_test.clj:

(ns com.getorcha.pdf.renderer-test
  (:require [clojure.test :refer [deftest is testing]]
            [com.getorcha.pdf.renderer :as pdf.renderer]))


(deftest render-to-pdf-test
  (testing "renders minimal HTML to PDF bytes"
    (let [renderer (pdf.renderer/open-html-to-pdf-renderer)
          html     (str "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
                        "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" "
                        "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">"
                        "<html xmlns=\"http://www.w3.org/1999/xhtml\">"
                        "<head><meta charset=\"UTF-8\"/></head>"
                        "<body><p>Hello</p></body></html>")
          result   (pdf.renderer/render-to-pdf renderer html {})]
      (is (bytes? result))
      (is (pos? (alength result)))
      ;; PDF magic bytes: %PDF-
      (is (= 0x25 (bit-and (aget result 0) 0xFF)))
      (is (= 0x50 (bit-and (aget result 1) 0xFF)))))

  (testing "renders with SVG support when enabled"
    (let [renderer (pdf.renderer/open-html-to-pdf-renderer {:svg? true})
          html     (str "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
                        "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" "
                        "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">"
                        "<html xmlns=\"http://www.w3.org/1999/xhtml\">"
                        "<head><meta charset=\"UTF-8\"/></head>"
                        "<body>"
                        "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"10\" height=\"10\">"
                        "<circle cx=\"5\" cy=\"5\" r=\"4\"/></svg>"
                        "</body></html>")
          result   (pdf.renderer/render-to-pdf renderer html {})]
      (is (bytes? result))
      (is (pos? (alength result))))))

Step 2: Run test to verify it fails

clj -X:test:silent :nses '[com.getorcha.pdf.renderer-test]'

Expected: FAIL — namespace com.getorcha.pdf.renderer doesn't exist.

Step 3: Write the implementation

Create src/com/getorcha/pdf/renderer.clj:

(ns com.getorcha.pdf.renderer
  "Shared PDF rendering protocol and implementations.

   Abstracts the HTML-to-PDF conversion behind a protocol so the rendering
   engine can be swapped (e.g., from OpenHTMLToPDF to Gotenberg) without
   changing callers."
  (:import (com.openhtmltopdf.pdfboxout PdfRendererBuilder)
           (com.openhtmltopdf.svgsupport BatikSVGDrawer)
           (com.openhtmltopdf.util XRLog)
           (java.io ByteArrayOutputStream)))


;; Disable verbose OpenHTMLToPDF logging (uses JUL, not SLF4J)
(XRLog/setLoggingEnabled false)


(defprotocol PdfRenderer
  (render-to-pdf [this html-string options]
    "Converts an XHTML string to PDF bytes.

     Arguments:
       html-string - Well-formed XHTML document string
       options     - Map of rendering options (currently unused, reserved for
                     future page-size/margin overrides)

     Returns PDF as byte array."))


(defrecord OpenHtmlToPdfRenderer [svg?]
  PdfRenderer
  (render-to-pdf [_this html-string _options]
    (with-open [os (ByteArrayOutputStream.)]
      (let [builder (PdfRendererBuilder.)]
        (.useFastMode builder)
        (when svg?
          (.useSVGDrawer builder (BatikSVGDrawer.)))
        (.withHtmlContent builder html-string nil)
        (.toStream builder os)
        (.run builder))
      (.toByteArray os))))


(defn open-html-to-pdf-renderer
  "Creates an OpenHTMLToPDF-based renderer.

   Options:
     :svg? - Enable SVG rendering via BatikSVGDrawer (default false)"
  ([] (->OpenHtmlToPdfRenderer false))
  ([{:keys [svg?] :or {svg? false}}]
   (->OpenHtmlToPdfRenderer svg?)))

Step 4: Run test to verify it passes

clj -X:test:silent :nses '[com.getorcha.pdf.renderer-test]'

Expected: PASS

Step 5: Refactor workers.ap.acquisition.html to use shared renderer

In src/com/getorcha/workers/ap/acquisition/html.clj:

The clean-html, wrap-html, wrap-email-html functions stay — they're content-specific, not rendering-specific.

Step 6: Refactor integrations.ap.cover-page to use shared renderer

In src/com/getorcha/integrations/ap/cover_page.clj:

Step 7: Run existing tests to verify no regressions

clj -X:test:silent 2>&1 | grep -A 5 -E "(FAIL in|ERROR in|Execution error|failed because|Ran .* tests)"

Expected: All tests pass.

Step 8: Lint

clj-kondo --lint src test dev

Fix any issues.

Step 9: Commit

git add src/com/getorcha/pdf/renderer.clj test/com/getorcha/pdf/renderer_test.clj src/com/getorcha/workers/ap/acquisition/html.clj src/com/getorcha/integrations/ap/cover_page.clj
git commit -m "refactor: extract shared PdfRenderer protocol from AP callsites"

Task 2: Split Invoice Structured Data Schema

Extract shared invoice fields into schema.invoice.common, keep pipeline-specific fields in a renamed schema.invoice.incoming, create schema.invoice.outgoing.

Files:

Important note on file rename: Don't literally rename the file yet — first create common.clj and outgoing.clj, then create incoming.clj that imports from common, then update structured_data.clj references. Finally, move the remaining incoming-specific types from the old file into incoming.clj and delete the old file. This avoids breaking everything at once.

Step 1: Write tests for common schema

Create test/com/getorcha/schema/invoice/common_test.clj:

(ns com.getorcha.schema.invoice.common-test
  (:require [clojure.test :refer [deftest is testing]]
            [com.getorcha.schema.invoice.common :as invoice.common]
            [malli.core :as m]))


(deftest common-line-item-test
  (testing "validates a minimal line item"
    (is (m/validate invoice.common/LineItem
                    {:description "Consulting"
                     :quantity    10
                     :unit-price  100.0
                     :tax-rate    19.0
                     :amount      1190.0})))

  (testing "rejects line item without description"
    (is (not (m/validate invoice.common/LineItem
                         {:quantity 1 :amount 100})))))


(deftest common-invoice-data-test
  (testing "validates minimal outgoing-compatible invoice"
    (is (m/validate invoice.common/CommonInvoiceData
                    {:invoice-number "INV-001"
                     :invoice-date   "2026-03-14"
                     :total          1190.0
                     :issuer         {:name "Orcha GmbH"}
                     :line-items     [{:description "Service" :amount 1000.0}]})))

  (testing "validates full invoice with all optional fields"
    (is (m/validate invoice.common/CommonInvoiceData
                    {:invoice-number "INV-002"
                     :invoice-date   "2026-03-14"
                     :total          2380.0
                     :issuer         {:name    "Orcha GmbH"
                                      :country "DE"
                                      :address "Berlin"
                                      :iban    "DE89370400440532013000"}
                     :recipient      {:name    "Client Corp"
                                      :country "US"}
                     :currency       "EUR"
                     :subtotal       2000.0
                     :discount       0
                     :tax-rate       19.0
                     :tax-amount     380.0
                     :amount-due     2380.0
                     :due-date       "2026-04-14"
                     :payment-terms  "Net 30"
                     :notes          "Thank you"
                     :line-items     [{:description "Consulting" :quantity 20 :unit-price 100.0 :amount 2000.0}]}))))

Step 2: Run test to verify it fails

clj -X:test:silent :nses '[com.getorcha.schema.invoice.common-test]'

Step 3: Create schema.invoice.common

Create src/com/getorcha/schema/invoice/common.clj. Extract from the current structured_data.clj:

The common LineItem has: description, article-code, quantity, unit, unit-price, price-per, quantity-multiplier, discount, discount-type, tax-rate, amount, category, surcharges. No post-processing fields.

The common CommonInvoiceData has: invoice-number, invoice-date, total, issuer, service-period, po-references, gr-references, recipient, shipping-address, shipping-country, currency, subtotal, discount, shipping, packaging, surcharges, tax-rate, tax-amount, tax-rate-breakdowns, prepayments, installments, amount-due, due-date, payment-reference, payment-terms, skonto-percentage, skonto-deadline, paid-date, incoterm-code, incoterm-place, freight-included, packaging-included, customs-included, insurance-included, delivery-terms-raw, line-items (using common LineItem), compliance-statements, notes (new field for outgoing). Use :maybe for optional fields, matching the existing pattern.

Step 4: Run tests to verify they pass

clj -X:test:silent :nses '[com.getorcha.schema.invoice.common-test]'

Step 5: Create schema.invoice.outgoing

Create src/com/getorcha/schema/invoice/outgoing.clj:

(ns com.getorcha.schema.invoice.outgoing
  "Schema for outgoing (AR) invoice structured data.
   Wraps common invoice fields with AR-specific additions."
  (:require [com.getorcha.schema.invoice.common :as invoice.common]
            [malli.core :as m]
            [malli.util :as mu]))


(def OutgoingInvoiceData
  "Schema for outgoing invoice structured data.
   Extends CommonInvoiceData with AR-specific fields."
  (m/schema
   (mu/merge
    invoice.common/CommonInvoiceData
    [:map
     [:internal-notes [:maybe :string]]])))

Step 6: Write test for outgoing schema

Create test/com/getorcha/schema/invoice/outgoing_test.clj:

(ns com.getorcha.schema.invoice.outgoing-test
  (:require [clojure.test :refer [deftest is testing]]
            [com.getorcha.schema.invoice.outgoing :as invoice.outgoing]
            [malli.core :as m]))


(deftest outgoing-invoice-data-test
  (testing "validates outgoing invoice with internal notes"
    (is (m/validate invoice.outgoing/OutgoingInvoiceData
                    {:invoice-number "INV-001"
                     :invoice-date   "2026-03-14"
                     :total          1000.0
                     :issuer         {:name "Orcha GmbH"}
                     :line-items     [{:description "Service" :amount 1000.0}]
                     :internal-notes "Customer requested rush delivery"})))

  (testing "validates without internal notes"
    (is (m/validate invoice.outgoing/OutgoingInvoiceData
                    {:invoice-number "INV-002"
                     :invoice-date   "2026-03-14"
                     :total          500.0
                     :issuer         {:name "Orcha GmbH"}
                     :line-items     [{:description "Widgets" :quantity 5 :unit-price 100.0 :amount 500.0}]
                     :internal-notes nil}))))

Step 7: Run all schema tests

clj -X:test:silent :nses '[com.getorcha.schema.invoice.common-test com.getorcha.schema.invoice.outgoing-test]'

Step 8: Create schema.invoice.incoming

Create src/com/getorcha/schema/invoice/incoming.clj. This wraps CommonInvoiceData with the pipeline-specific types that remain in structured_data.clj:

Step 9: Update schema.structured-data dispatch

In src/com/getorcha/schema/structured_data.clj:

Step 10: Update all requires of schema.invoice.structured-data

Search for all files requiring com.getorcha.schema.invoice.structured-data and update them to require com.getorcha.schema.invoice.incoming instead. The alias should be invoice.incoming.

Step 11: Delete src/com/getorcha/schema/invoice/structured_data.clj

Once all references point to incoming, remove the old file.

Step 12: Run full test suite

clj -X:test:silent 2>&1 | grep -A 5 -E "(FAIL in|ERROR in|Execution error|failed because|Ran .* tests)"

Step 13: Lint

clj-kondo --lint src test dev

Step 14: Commit

git add src/com/getorcha/schema/invoice/common.clj src/com/getorcha/schema/invoice/outgoing.clj src/com/getorcha/schema/invoice/incoming.clj test/com/getorcha/schema/invoice/common_test.clj test/com/getorcha/schema/invoice/outgoing_test.clj src/com/getorcha/schema/structured_data.clj
git rm src/com/getorcha/schema/invoice/structured_data.clj  # if applicable
git add -u  # catch files that changed their requires
git commit -m "refactor: split invoice schema into common/incoming/outgoing"

Task 3: Database Migration + Codebase-Wide Rename

Rename invoiceincoming-invoice in PostgreSQL enum and all Clojure code. Add outgoing-invoice enum value. Make content_hash and file_path nullable. Add status column.

Files:

Step 1: Create the migration

Use the current timestamp for the migration filename (e.g., 20260314120000).

resources/migrations/YYYYMMDDHHMMSS-ar-document-type-rename.up.sql:

-- Add new enum values
ALTER TYPE document_type ADD VALUE IF NOT EXISTS 'incoming-invoice';
--;;
ALTER TYPE document_type ADD VALUE IF NOT EXISTS 'outgoing-invoice';
--;;
-- Rename existing invoice rows
UPDATE document SET type = 'incoming-invoice' WHERE type = 'invoice';
--;;
-- Make content_hash and file_path nullable for outgoing invoices
ALTER TABLE document ALTER COLUMN content_hash DROP NOT NULL;
--;;
ALTER TABLE document ALTER COLUMN file_path DROP NOT NULL;
--;;
-- Convert unique constraint on content_hash to partial index (exclude nulls)
ALTER TABLE document DROP CONSTRAINT IF EXISTS document_content_hash_key;
--;;
CREATE UNIQUE INDEX IF NOT EXISTS document_content_hash_unique ON document (content_hash) WHERE content_hash IS NOT NULL;
--;;
-- Add status column for AR workflow
CREATE TYPE ar_invoice_status AS ENUM ('draft', 'sent', 'paid');
--;;
ALTER TABLE document ADD COLUMN status ar_invoice_status;

Down migration reverses these changes (update incoming-invoice back to invoice, drop status column, restore NOT NULL, etc.).

Note: PostgreSQL doesn't support removing enum values. The down migration can update rows back but the enum values will persist. This is acceptable.

Step 2: Update init.sql for fresh installs

In resources/migrations/init.sql, update the document_type enum to include the new values and update any references from 'invoice' to 'incoming-invoice'. Add the ar_invoice_status type and status column to the document table. Make content_hash and file_path nullable. Replace the unique constraint with a partial unique index.

Step 3: Rename :invoice:incoming-invoice across the codebase

This is a mechanical find-and-replace with care to avoid false positives. Do NOT change :invoice-number, :invoice-date, :invoice-subtype, :invoice-description, :invoice-splitting, or any field name that starts with invoice-.

Schema files:

Worker files (defmethod dispatches):

Matching files:

UI files:

Test files (mechanical rename): Update all test files that reference "invoice" as a document type or :invoice as a keyword type. Main files:

Step 4: Run full test suite

clj -X:test:silent 2>&1 | grep -A 5 -E "(FAIL in|ERROR in|Execution error|failed because|Ran .* tests)"

This will catch any missed renames. Fix until green.

Step 5: Lint

clj-kondo --lint src test dev

Step 6: Commit

git add resources/migrations/ src/ test/
git commit -m "refactor: rename document type invoice to incoming-invoice, add outgoing-invoice"

Task 4: Default Invoice Template

Create the multimethod-based template system and a default invoice template that renders hiccup.

Files:

Step 1: Write the failing test

Create test/com/getorcha/app/http/ar/template_test.clj:

(ns com.getorcha.app.http.ar.template-test
  (:require [clojure.test :refer [deftest is testing]]
            [com.getorcha.app.http.ar.template :as ar.template]))


(deftest render-invoice-template-test
  (testing "default template returns hiccup for minimal invoice"
    (let [invoice-data {:tenant-id      (random-uuid)
                        :invoice-number "INV-001"
                        :invoice-date   "2026-03-14"
                        :total          1190.0
                        :currency       "EUR"
                        :issuer         {:name "Orcha GmbH" :address "Berlin, Germany"}
                        :recipient      {:name "Client Corp" :address "New York, US"}
                        :line-items     [{:type        :product
                                          :description "Consulting"
                                          :quantity    10
                                          :unit-price  100.0
                                          :tax-rate    19.0
                                          :amount      1000.0}]
                        :subtotal       1000.0
                        :tax-amount     190.0}
          result       (ar.template/render-invoice-template invoice-data)]
      ;; Result should be a hiccup vector
      (is (vector? result))
      ;; Should start with an element tag
      (is (keyword? (first result)))))

  (testing "template renders section and note line types"
    (let [invoice-data {:tenant-id      (random-uuid)
                        :invoice-number "INV-002"
                        :invoice-date   "2026-03-14"
                        :total          1000.0
                        :issuer         {:name "Test Co"}
                        :line-items     [{:type :section :description "Professional Services"}
                                         {:type :product :description "Dev work" :quantity 8 :unit-price 125.0 :amount 1000.0}
                                         {:type :note :description "Billed at agreed hourly rate"}]}
          result       (ar.template/render-invoice-template invoice-data)]
      (is (vector? result)))))


(deftest invoice-html-test
  (testing "converts template hiccup to XHTML string"
    (let [invoice-data {:tenant-id      (random-uuid)
                        :invoice-number "INV-001"
                        :invoice-date   "2026-03-14"
                        :total          100.0
                        :issuer         {:name "Test"}
                        :line-items     []}
          html         (ar.template/invoice-html invoice-data)]
      (is (string? html))
      (is (clojure.string/includes? html "INV-001"))
      (is (clojure.string/includes? html "<?xml")))))

Step 2: Run test to verify it fails

clj -X:test:silent :nses '[com.getorcha.app.http.ar.template-test]'

Step 3: Implement the template

Create src/com/getorcha/app/http/ar/template.clj.

This namespace contains:

The default template should render a professional A4 invoice with:

Style with CSS 2.1 compatible rules (tables, floats, no flexbox/grid). Use @page { size: A4; margin: 15mm; } for consistent sizing.

The invoice-html function:

  1. Calls render-invoice-template to get hiccup
  2. Wraps in XHTML document with proper doctype and CSS
  3. Converts to string via hiccup2.core/html

Step 4: Run tests

clj -X:test:silent :nses '[com.getorcha.app.http.ar.template-test]'

Step 5: Lint

clj-kondo --lint src test dev

Step 6: Commit

git add src/com/getorcha/app/http/ar/template.clj test/com/getorcha/app/http/ar/template_test.clj
git commit -m "feat: add default AR invoice template with hiccup multimethod"

Task 5: AR Invoice Create/Edit Form + Live Preview

Build the invoice creation/edit form with split-pane layout and live PDF preview.

Files:

Step 1: Write handler tests

Create test/com/getorcha/app/http/ar_test.clj:

(ns com.getorcha.app.http.ar-test
  (:require [clojure.test :refer [deftest is testing use-fixtures]]
            [com.getorcha.test.fixtures :as fixtures]))


(use-fixtures :once fixtures/system-fixture)
(use-fixtures :each fixtures/rollback-fixture)


(deftest create-page-test
  (testing "GET /ar/create returns 200 with form"
    (let [resp (fixtures/request {:route  :com.getorcha.app.http.ar/create
                                  :method :get})]
      (is (= 200 (:status resp))))))


(deftest preview-test
  (testing "POST /ar/preview returns PDF bytes"
    (let [resp (fixtures/request {:route        :com.getorcha.app.http.ar/preview
                                  :method       :post
                                  :content-type "application/x-www-form-urlencoded"
                                  :body         "invoice-number=INV-001&invoice-date=2026-03-14&total=1000"})]
      (is (= 200 (:status resp)))
      (is (= "application/pdf" (get-in resp [:headers "Content-Type"]))))))


(deftest save-invoice-test
  (testing "POST /ar/invoices persists structured data"
    (let [resp (fixtures/request {:route        :com.getorcha.app.http.ar/save
                                  :method       :post
                                  :content-type "application/x-www-form-urlencoded"
                                  :body         "invoice-number=INV-001&invoice-date=2026-03-14&total=1000&recipient-name=Client+Corp"})
          ;; Verify document was created
          docs (fixtures/query "SELECT * FROM document WHERE type = 'outgoing-invoice'")]
      (is (= 302 (:status resp)))  ;; Redirect to edit page
      (is (= 1 (count docs)))
      (is (= "INV-001" (get-in (first docs) [:structured_data :invoice-number]))))))

These tests will need adjustment based on your actual fixtures API. They outline the expected behavior.

Step 2: Implement app.http.ar

Create src/com/getorcha/app/http/ar.clj with:

Namespace requires:

Module-level state:

(def ^:private renderer (pdf.renderer/open-html-to-pdf-renderer))

Helper: form-params->invoice-data Coerces flat form params into the outgoing invoice structured data shape. Parses numbers, builds line items from indexed params (e.g., line-items[0][description]), sets issuer from tenant/legal entity config.

Handler: create-page (GET /ar/create) Returns the split-pane form layout:

Handler: edit-page (GET /ar/invoices/:id) Same form layout but pre-populated with existing invoice data from database.

Handler: preview (POST /ar/preview)

  1. Parse form data via form-params->invoice-data
  2. Call (ar.template/invoice-html invoice-data) to get XHTML
  3. Call (pdf.renderer/render-to-pdf renderer xhtml {}) to get PDF bytes
  4. Return response with Content-Type: application/pdf and Content-Disposition: inline

Handler: save-invoice (POST /ar/invoices)

  1. Parse form data
  2. Insert into document table with type = 'outgoing-invoice', status = 'draft', structured_data = invoice-data-as-json
  3. Redirect to edit page for the new invoice

Handler: update-invoice (PUT /ar/invoices/:id)

  1. Parse form data
  2. Update document.structured_data and document.updated_at
  3. Return updated form (HTMX swap)

UI: Form layout

The form hiccup structure:

[:div.ar-editor
  [:div.ar-form-pane
    ;; Header fields (grid layout)
    ;; Line items table (editable)
    ;; Totals section
    ;; Notes section
    ;; Save button]
  [:div.ar-preview-pane
    [:iframe#preview-frame {:src "/ar/preview"}]]]

Key HTMX patterns:

Step 3: Wire routes

Add to the routes function at the bottom of ar.clj:

(defn routes [_config]
  ["/ar"
   [""
    {:name ::list
     :get  {:handler #'list-invoices}}]
   ["/create"
    {:name ::create
     :get  {:handler #'create-page}}]
   ["/preview"
    {:name ::preview
     :post {:handler #'preview}}]
   ["/invoices"
    {:name ::save
     :post {:handler #'save-invoice}}]
   ["/invoices/:id"
    [""
     {:name ::edit
      :get  {:handler #'edit-page}
      :put  {:handler #'update-invoice}}]]])

Step 4: Register routes in app.http

In src/com/getorcha/app/http.clj:

Step 5: Add nav item

In src/com/getorcha/app/ui/layout.clj:

Step 6: Add CSS

Add AR-specific styles to resources/app/public/css/style.css:

Step 7: Run tests

clj -X:test:silent :nses '[com.getorcha.app.http.ar-test]'

Step 8: Lint

clj-kondo --lint src test dev

Step 9: Commit

git add src/com/getorcha/app/http/ar.clj src/com/getorcha/app/http.clj src/com/getorcha/app/ui/layout.clj test/com/getorcha/app/http/ar_test.clj resources/app/public/css/style.css
git commit -m "feat: AR invoice create/edit form with live PDF preview"

Task 6: AR Overview/List Screen

Build the list page mirroring AP's structure.

Files:

Step 1: Add list test

Add to test/com/getorcha/app/http/ar_test.clj:

(deftest list-invoices-test
  (testing "GET /ar returns 200 with empty list"
    (let [resp (fixtures/request {:route  :com.getorcha.app.http.ar/list
                                  :method :get})]
      (is (= 200 (:status resp)))))

  (testing "GET /ar shows created invoices"
    ;; Create an invoice first
    (fixtures/request {:route        :com.getorcha.app.http.ar/save
                       :method       :post
                       :content-type "application/x-www-form-urlencoded"
                       :body         "invoice-number=INV-001&invoice-date=2026-03-14&total=1000&recipient-name=Client+Corp"})
    (let [resp (fixtures/request {:route  :com.getorcha.app.http.ar/list
                                  :method :get})]
      (is (= 200 (:status resp)))
      ;; Response body should contain the invoice number
      (is (clojure.string/includes? (str (:body resp)) "INV-001")))))

Step 2: Implement list-invoices handler

In src/com/getorcha/app/http/ar.clj, add:

Query function:

(defn ^:private query-outgoing-invoices
  [db-pool legal-entity-ids {:keys [status page sort-by sort-dir]}]
  ;; HoneySQL query:
  ;; SELECT document.id, document.created_at, document.structured_data,
  ;;        document.status, legal_entity.name
  ;; FROM document
  ;; JOIN legal_entity ON legal_entity.id = document.legal_entity_id
  ;; WHERE document.type = 'outgoing-invoice'
  ;;   AND document.legal_entity_id IN (...)
  ;;   AND (status filter if provided)
  ;; ORDER BY (sort column) (sort dir)
  ;; LIMIT 100 OFFSET (page * 100)
  )

List page renderer: Mirrors AP's list-page function. Renders:

Invoice row renderer: Similar to AP's document-row. Extracts fields from structured_data:

Status badge renders draft/sent/paid with appropriate colors.

Step 3: Run tests

clj -X:test:silent :nses '[com.getorcha.app.http.ar-test]'

Step 4: Lint

clj-kondo --lint src test dev

Step 5: Commit

git add src/com/getorcha/app/http/ar.clj test/com/getorcha/app/http/ar_test.clj
git commit -m "feat: AR overview/list screen mirroring AP structure"

Task 7: Polish + Integration Testing

Visual polish, edge cases, and end-to-end verification.

Step 1: Manual testing checklist

Start the system ((go) in REPL) and verify:

Step 2: Fix CSS and layout issues

Address any visual issues found during manual testing. Ensure:

Step 3: Run full test suite one final time

clj -X:test:silent 2>&1 | grep -A 5 -E "(FAIL in|ERROR in|Execution error|failed because|Ran .* tests)"

Step 4: Lint

clj-kondo --lint src test dev

Step 5: Final commit

git add -u
git commit -m "feat: AR module polish and integration fixes"