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.

Recipient Identity Validation Against Legal Entity Master Data

Issue: getorcha/orcha#335 Date: 2026-03-31

Problem

The AP ingestion pipeline validates the issuer/supplier side thoroughly (VAT format, VIES lookup, master data matching, fraud detection) but only performs presence checks on the recipient. An invoice addressed to a completely different company passes validation as long as name and address fields aren't blank.

Per UStG §14 Abs. 4, an invoice must contain the correct full name and address of the recipient (Leistungsempfänger).

Design

Fetch the legal entity record once in job-handler immediately after claim-ingestion! succeeds. Store it in the pipeline state under :legal-entity so all downstream stages can access it without redundant queries.

Selected columns: name, company-address, company-vat-id, company-tax-id, company-country.

Key files:

Change validate multimethod signature

Add legal-entity as a second argument to the validate multimethod and all its implementations:

(defmulti validate
  (fn [structured-data _legal-entity] (:document-type structured-data)))

with-validations in ingestion.clj:336 passes (:legal-entity ingestion) as the second arg. Non-invoice document types receive it but ignore it.

New check: check-recipient-identity

A deterministic validation check in validation.clj that compares recipient fields from the invoice against the legal entity master data.

Per-field comparison

Field Invoice source LE source Normalization Pass condition
Name (:name recipient) :legal-entity/name Whitespace collapse + trim Exact match
VAT ID (:vat-id recipient) or (:tax-id recipient) when (:tax-id-type recipient) = "vat" :legal-entity/company-vat-id util.tax/normalize-vat-id Exact match
Tax ID (:tax-id recipient) when type ≠ "vat" :legal-entity/company-tax-id Uppercase, strip separators ([\s.\-/,]) Exact match
Address (:address recipient) :legal-entity/company-address Lowercase, strip punctuation, collapse whitespace Exact match
Country (:country recipient) :legal-entity/company-country Uppercase ISO code Exact match

Skip logic per field

Result

The :details map keys are :name, :vat-id, :tax-id, :address, :country. Each value is :match, :mismatch, or :skip.

Interaction with existing checks

Existing check Recipient fields Status if missing Overlap with new check
check-required-fields recipient.name, recipient.address "uncertain" (for >€250) None — existing checks flag absence, new check flags incorrectness
check-recipient-country recipient.country "uncertain" None — existing check flags missing country, new check flags wrong country

No overlap: the new check only fires when a field is present but doesn't match.

UVR integration for vision-assisted resolution

Extend UncertainValidationsResolver to also pick up :recipient-identity checks with "warning" status. This is a targeted exception to UVR's default "uncertain" filter — only for this specific check.

The filter change in uncertain_validations.clj:172-177:

;; Before:
(and (= "uncertain" (:status v))
     (not= k :financial-math))

;; After:
(and (not= k :financial-math)
     (or (= "uncertain" (:status v))
         (and (= "warning" (:status v))
              (= k :recipient-identity))))

UVR instruction block

New recipient-identity-instruction constant:

**Recipient identity questions**:
The extracted recipient data does not match the legal entity master data.
Look at the actual recipient block on the attached PDF and verify each mismatched field.
If the PDF shows the correct legal entity data and the extraction had an error,
provide corrections to fix the extracted data.
If the recipient on the PDF genuinely differs from the legal entity, confirm the mismatch.
Return: {"recipient-identity": {"value": "pass"|"warning", "confidence": 0.9,
  "reasoning": "...", "corrections": {"recipient.name": "..."}}}

Vision mode

Recipient identity resolution sets needs-pdf? = true since it needs to read the recipient block from the actual document. UVR sends relevant pages and uses the vision LLM config.

needs-pdf? condition

Extend the existing condition to include recipient identity:

;; Before:
needs-pdf? (or has-iban? has-required-fields?)

;; After:
needs-pdf? (or has-iban? has-required-fields? has-recipient-identity?)

Correction field paths

Add recipient fields to the common/correction-field-paths allowlist so UVR can apply corrections: "recipient.name", "recipient.address", "recipient.country", "recipient.vat-id", "recipient.tax-id".

UVR apply logic

UVR receives the ingestion map in its constructor, which now includes :legal-entity. After applying corrections, it can re-run check-recipient-identity with the LE data from (:legal-entity ingestion).

In the -apply method, when check-name = :recipient-identity:

Pipeline flow

job-handler
  → claim-ingestion! → fetch LE record → assoc :legal-entity into pipeline-state
  → fetch-files-from-s3!
  → transcribe!
  → classify!
  → extract! (reads :legal-entity for prompt context, no more DB fetch)
  → with-validations
      → validate(structured-data, legal-entity)
          → check-recipient-identity: compare recipient vs LE
              → pass: all fields match
              → warning: mismatch detected, details map included
  → post-process!
      → ... other processors ...
      → UncertainValidationsResolver
          → picks up :recipient-identity warning
          → vision LLM reviews PDF recipient block
              → OCR error? → corrections applied, status → pass
              → real mismatch? → warning persists → user sees it in UI