AP Processing Modes Design

Context

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.

Goals

Non-Goals

Data Model

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_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.

Mode Semantics

read_only

human_review_export

Approvals and inline editing are one capability. There is no supported mode with approvals but no edits, or edits but no approvals.

straight_through_export

Processing Terminal Point

Straight-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:

The 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.

Output Requests

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:

The 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.

Authorization

Create one shared AP output authorization layer and use it from:

Rules:

Manual endpoints must enforce these checks server-side. Hiding a button in the UI is only presentation.

DATEV Eligibility

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.

Admin UI

Replace the tenant "AP Approvals" section with "AP Processing" on the tenant detail page.

The section shows:

The 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.

Document UI

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:

Event Flow

New Invoice, read_only

  1. Ingestion completes.
  2. No approval rows are created.
  3. Matching/reconciliation may run.
  4. No output job is created.

New Invoice, human_review_export

  1. Ingestion completes.
  2. The current approver roster is snapshotted into ap_invoice_approval.
  3. Matching/reconciliation runs.
  4. Users can edit while the invoice has approval rows.
  5. On final approval, the approval handler calls the shared AP output request function.
  6. If authorized and no active output job exists, a document_output_job is created with trigger approval_completed and sent to SQS.

New Invoice, straight_through_export

  1. Ingestion completes.
  2. No approval rows are created.
  3. Matching/reconciliation runs.
  4. When matching terminal handling runs, it calls the shared AP output request function.
  5. If authorized and no active output job exists, a document_output_job is created with trigger processing_completed and sent to SQS.

Manual Export/Re-Export

  1. The route verifies document access.
  2. The route calls the shared AP output authorization function with trigger manual.
  3. If authorized and no active output job exists, a document_output_job is created and sent to SQS.
  4. If not authorized, the route returns a forbidden or conflict response that the UI can render inline.

Rollout And Migration

Create an initial config row for each existing tenant. The migration must map the existing approval boolean into the new mode:

Use 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.

Testing

Add tests for:

Open Risks