Note (2026-04-24): After this document was written,
legal_entitywas renamed totenantand the oldtenantwas renamed toorganization. Read references to these terms with the pre-rename meaning.
Orcha's first feature was Accounts Payable (AP) — ingesting, classifying, and matching incoming invoices. We're now adding Accounts Receivable (AR) as a parallel module, starting with the UI for creating and managing outgoing invoices.
AR will eventually have its own ingestion pipeline (external systems pushing/pulling data to create draft invoices), but this iteration focuses on the UI and data model foundation.
Document type rename: The document_type enum currently has invoice (implicitly incoming). Rename to incoming-invoice and add outgoing-invoice.
Migration steps:
incoming-invoice and outgoing-invoice enum valuesUPDATE document SET type = 'incoming-invoice' WHERE type = 'invoice'content_hash and file_path nullable (outgoing invoices start as structured data, not files)content_hash unique constraint to partial unique index excluding nullsstatus column to document table for AR workflow: draft, sent, paid:invoice type to :incoming-invoiceWhy same table: AR invoices will also have matching (to contracts), and the document table already holds multiple types (contracts, POs, GRNs). A separate table would need to mirror the same structure. The type enum already discriminates document types — an outgoing invoice is just another type.
Three layers:
schema.invoice.common — shared fields: invoice-number, invoice-date, total, subtotal, issuer, recipient, line-items, currency, discount, tax-rate-breakdowns, due-date, payment-terms, amount-due, notes, shipping, prepayments, installmentsschema.invoice.incoming — wraps common + pipeline outputs: confidence, fraud-flags, tax-issues, validation-results, service-category, and line item post-processing fields (debit-account, credit-account, accrual, cost-center, vat-validation, bu-code)schema.invoice.outgoing — wraps common + internal-notes (minimal for now, no speculative fields)Protocol: PdfRenderer with render-to-pdf method taking an HTML string and options map, returning PDF bytes. Single implementation: OpenHtmlToPdfRenderer wrapping the existing PdfRendererBuilder pattern.
Shared across codebase: Extract from existing duplicated code in:
workers.ap.acquisition.html (email/Excel → PDF)integrations.ap.cover_page (DATEV validation cover page)All three refactored to use the shared renderer.
Why OpenHTMLToPDF: Already in deps.edn, already used in two places, pure JVM (no infrastructure), fast enough for live preview (~50-100ms). Coded behind a protocol so the engine is replaceable if needed.
Multimethod dispatching on tenant, returning hiccup:
(defmulti render-invoice-template :tenant-id)
(defmethod render-invoice-template :default [invoice-data]
[:div.invoice ...])
Custom templates = new defmethod per tenant, converting customer-provided HTML to hiccup as code. Hiccup → HTML string → OpenHTMLToPDF → PDF bytes.
No template engine (no Selmer), no S3 storage, no runtime loading, no template builder. Default template ships with Orcha.
hx-post with throttle:300ms sends form data to serverContent-Type: application/pdf)Target round-trip: under 500ms.
Layout: Split pane — form on the left, PDF preview on the right. Responsive: preview collapses or moves below on small screens.
Form fields:
Header:
Line items table (editable rows):
Totals (calculated, readonly):
Notes:
Issuer comes from tenant/legal entity config, not the form.
Mirrors AP list structure.
Columns:
Filters:
Sorting: Default by created date descending. Clickable column headers.
Actions:
New namespaces:
com.getorcha.app.http.ar — list page, create/edit form, preview endpointcom.getorcha.schema.invoice.common — shared invoice fieldscom.getorcha.schema.invoice.incoming — incoming-specific (replaces current schema.invoice.structured-data)com.getorcha.schema.invoice.outgoing — outgoing-specificcom.getorcha.pdf.renderer — PdfRenderer protocol + OpenHtmlToPdfRendererModified namespaces:
com.getorcha.schema.structured-data — update dispatch for incoming-invoice / outgoing-invoicedocument.type = :invoice → :incoming-invoiceworkers.ap.acquisition.html — refactor to use shared rendererintegrations.ap.cover_page — refactor to use shared rendererNot created this iteration:
workers/ar/ — no ingestion pipelineintegrations/ar/ — no export/send