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.
Issue: getorcha/orcha#335 Date: 2026-03-31
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).
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:
ingestion.clj:830-831 — fetch and assoc :legal-entity into pipeline-stateextraction.clj:1324-1328 — remove redundant LE fetch, read from (:legal-entity ingestion)tax_compliance.clj:304-308 — remove redundant LE fetch, read from (:legal-entity ingestion)validate multimethod signatureAdd 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.
check-recipient-identityA deterministic validation check in validation.clj that compares recipient fields from the invoice against the legal entity master data.
| 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:skipname is NOT NULL — only skips if invoice recipient name is blank{:status "pass"}{:status "warning", :message "Recipient does not match legal entity master data", :details {:name :match, :vat-id :mismatch, :address :skip, ...}}The :details map keys are :name, :vat-id, :tax-id, :address, :country. Each value is :match, :mismatch, or :skip.
| 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.
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))))
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": "..."}}}
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? conditionExtend 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?)
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 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:
value = "pass" with corrections → apply corrections to structured-data, re-run check-recipient-identity (passing LE from ingestion), update validation result to passvalue = "warning" → leave the warning in place (real mismatch confirmed)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