AP invoice behavior is currently controlled by organically grown checks around
DATEV connectivity and tenant_ap_approval_config.is_enabled. That toggle now
controls more than approvals: it also gates inline editing, approval snapshots,
and automatic DATEV export after final approval. This makes the business policy
implicit and spread across app handlers, ingestion workers, and export code.
We want an explicit tenant-level AP processing mode that describes how invoices move through Orcha:
This design also builds on the document output dispatcher design: app code creates generic output jobs, and workers execute external outputs. DATEV remains the first output implementation, but AP processing policy must not be DATEV specific.
Add an append-only tenant AP processing configuration table.
CREATE TYPE ap_processing_mode AS ENUM (
'read_only',
'human_review_export',
'straight_through_export'
);
CREATE TABLE tenant_ap_processing_config (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenant(id) ON DELETE CASCADE,
mode ap_processing_mode NOT NULL,
created_by UUID REFERENCES "identity"(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_tenant_ap_processing_config_latest
ON tenant_ap_processing_config (tenant_id, created_at DESC, id DESC);
The current mode is the latest row for the tenant ordered by
created_at DESC, id DESC. A tenant with no config row is treated as
read_only.
The table is append-only. Saving a new mode inserts a row; old rows remain for auditability. This setting controls whether invoices can leave Orcha, so the history of who changed it and when should be durable.
Existing tables remain responsible for their current domains:
tenant_ap_approver stores the reusable approver roster.ap_invoice_approval stores per-invoice approval snapshots.ap_invoice_approval_event stores approval-row audit events.document_output_job stores generic output attempts.ap_datev_export_audit stores DATEV-specific export task state.tenant_ap_approval_config is legacy policy state. It should be used only to
backfill the initial tenant_ap_processing_config rows and then removed from
runtime decisions. The implementation should either drop the table after code no
longer references it, or leave it unused only as temporary rollback scaffolding.
It must not remain a second policy source.
read_onlyhuman_review_exportap_invoice_approval.Approvals and inline editing are one capability. There is no supported mode with approvals but no edits, or edits but no approvals.
straight_through_exportStraight-through automatic output must not run immediately after
complete-ingestion!, because matching and reconciliation can add diagnostics
that should be included in the export cover page.
The automatic output boundary is the matching worker's terminal handling for an invoice. Reconciliation runs synchronously inside the matching processor, so there is no separate reconciliation worker to wait for. At that point:
The matching worker should call the shared AP output request function from terminal paths only:
process-document! / engine/run-processors! returns successfully,
before deleting the SQS messagehandle-failure! has written a terminal failed matching runThe call should not live in process-document-with-notify!'s finally block,
because that block also runs for transient attempts that should not request
output yet. Non-matchable document types are marked with a skipped matching run;
the shared output request function still checks that the document is an invoice
before creating a job.
If the document is not an invoice, if there is no usable invoice payload, or if the tenant mode does not allow output, the function returns without creating a job.
If an invoice reached this terminal point before the tenant switched to
straight_through_export, the mode-change action does not auto-enqueue it.
Users may manually export that older terminal invoice after the switch.
Document output dispatch remains generic. AP processing policy decides when an invoice may request output; the document output worker decides which configured outputs to execute.
document_output_job.trigger should include:
approval_completedprocessing_completedmanualThe existing document output migration currently creates
document_output_trigger with only approval_completed and manual. Adding
processing_completed requires a new migration:
ALTER TYPE document_output_trigger ADD VALUE IF NOT EXISTS 'processing_completed';
This migration must land before any code attempts to insert
processing_completed jobs. PostgreSQL enum values added by ALTER TYPE may
not be usable until the transaction commits, so keep this as its own migration
step rather than combining it with inserts that use the new value.
The job table must not include a target. A job means "dispatch this document's configured outputs." DATEV is the first output implementation, but the model must support later outputs without changing the AP policy table.
Keep the active-job uniqueness rule from the output dispatcher design:
CREATE UNIQUE INDEX idx_document_output_job_document_active
ON document_output_job (document_id)
WHERE status IN ('pending', 'running');
This prevents duplicate exports from double clicks, duplicate events, or automatic/manual races.
Create one shared AP output authorization layer and use it from:
Rules:
read_only: never authorized.human_review_export: authorized only when the invoice has approval rows and
aggregate approval state is fully approved.straight_through_export: authorized when the invoice is terminal for output.pending and running jobs are active blockers.Manual endpoints must enforce these checks server-side. Hiding a button in the UI is only presentation.
DATEV eligibility should not check validation errors. Validation, matching, and reconciliation diagnostics are output content, not blockers.
DATEV-specific capability checks should only cover whether DATEV can physically receive the invoice:
The existing maesn/export-eligible? should be narrowed accordingly or replaced
with a better-named DATEV capability predicate. The implemented document output
worker currently calls this predicate before maesn/create-booking-proposal!;
that dispatch-time check must stop treating validation errors as blockers. The
invoice UI does not currently call maesn/export-eligible? directly, but its
export-button visibility must also be moved to the shared AP output
authorization rules so UI behavior matches the server-side route.
Replace the tenant "AP Approvals" section with "AP Processing" on the tenant detail page.
The section shows:
read_only, human_review_export, and
straight_through_exportThe approver roster is preserved across modes. It is active only when the mode
is human_review_export, but admins may still view and maintain it while the
tenant is in another mode.
Saving human_review_export requires at least one approver. Saving a mode
inserts a new tenant_ap_processing_config row.
Saving straight_through_export does not enqueue historical terminal invoices.
It only affects invoices that reach the terminal processing point after the mode
change. Older terminal invoices become manually exportable.
The invoice detail page derives behavior from the current tenant mode and the document's approval rows.
read_only:
human_review_export:
straight_through_export:
read_onlyhuman_review_exportap_invoice_approval.document_output_job is
created with trigger approval_completed and sent to SQS.straight_through_exportdocument_output_job is
created with trigger processing_completed and sent to SQS.manual.document_output_job is
created and sent to SQS.Create an initial config row for each existing tenant. The migration must map the existing approval boolean into the new mode:
tenant_ap_approval_config.is_enabled = true -> human_review_exportis_enabled = false -> read_onlyUse created_by = NULL for migration-created rows unless a reliable actor is
available. This preserves current approval-enabled tenants and prevents them
from silently falling back to read_only.
After backfill, tenant_ap_approval_config.is_enabled should stop being the
source of policy. Existing approval rows remain valid as historical per-invoice
review snapshots.
The migration should avoid deleting approver rosters. Approvers remain reusable tenant setup.
Add tests for:
read_onlyhuman_review_export without approvers is rejectedhuman_review_export invoices snapshot approval rowsread_only and straight_through_export invoices do not snapshot
approval rowsstraight_through_exportread_onlystraight_through_export does not auto-enqueue historical
terminal invoicesdocument_processor_run conventions. Failed
matching/reconciliation is terminal for output, not a blocker.