CATHAGO · altair engine v10 + checks-v1.0 · task CU-86c7x8puw

Invoice verification, the full reference.

The complete picture of the native invoice-verification feature — the matching engine and checks, the document lifecycle from inbox to approved, the reviewer workflow in kepler, and the domain model, config and known gaps underneath. Four tabs; every claim traced to the backend + frontend knowledge bases.

altair's in-process engine (ENGINE_VERSION "v10.0") matches every invoice line against the order, the delivery note and the agreed price list, then a checks engine (checks-v1.0) runs financial, tax, master-data, duplicate and temporal rules on top. Everything is gated per tenant and frozen into an append-only audit report.

3matching tracks
11+1verification checks
4ways to trigger a run
0n8n calls in the hot path
1.1

The three matching tracks

Each invoice line is matched per track by a normalized article key. All three converge on InvoiceMatchingService.match(companyAccountId, invoiceId, Set<MatchTrack>).

Order · FIND_ORDER

Invoice line ↔ order line

Candidates = referenced-order lines with the same normalized article key. Best = min |orderedQty − invoiceQty|. Quantity outside tolerance ⇒ MISMATCHED. A unit-price guard rejects a 0.00-priced order line vs a priced invoice line.

Anti-double-bill: order__line_consumption ledger — remaining = ordered − consumed by other invoices; no capacity ⇒ OVER_CONSUMPTION.

Delivery · FIND_DELIVERY

Invoice line ↔ delivery note

First article-key match across referenced deliveries; cross-supplier allowed. A quantity mismatch keeps the matched line so 3-way reconciliation can still read the delivered qty.

Scope: honours the workflow's "e-delivery note only" toggle — regular extracted notes skipped under that scope with a truthful verdict.

Price · PRICE

Invoice price ↔ price list

Resolution order: a manual PRICELIST reference → the line's own priceListId → master-catalog lookup. Compares invoice unit price vs the price entry's pricePerUnit within tolerance.

Usability gate: price list must be ACTIVE and assigned to this tenant+project. Unresolvable ⇒ WAITING (never a false FAIL).

3-way reconciliation. When order matched and a delivery line is known, the engine checks billed ≤ delivered ≤ orderedBILLED_MORE_THAN_DELIVERED is critical (paying for goods not received), DELIVERED_MORE_THAN_ORDERED a warning. Comparison is unit-of-measure aware: "200 KG" reconciles against "10 SAC" via the extracted alternative unit, then the master product's package factor; genuinely incomparable units emit an honest "differing units" verdict rather than a fabricated mismatch.

1.2

The run, stage by stage

STAGE 1

Trigger

Kafka event on invoice create/update, or a reviewer hits Run / Re-run.

InvoiceVerificationListener
STAGE 2

Gate

Which of the 3 tracks may run for this tenant, project, supplier & status.

AutomationGateService
STAGE 3

Match

Per line: order → delivery → price; reconcile; write per-track verdicts + ledger + references.

InvoiceMatchingServiceImpl
STAGE 4

Checks

Reload the doc, run all 11 checks + the 3-way quantity check sequentially.

VerificationCheck beans
STAGE 5

Report

Compute counters + outcome, append a report, log a Timeline activity.

VerificationReportEntity
STAGE 6

Approve

Reviewer checklist gates Approve — or touchless auto-approve if configured.

InvoiceAutoApproveService

Four ways a run starts

Auto

Invoice created / updated

Kafka listener on InvoiceBusinessDocumentTopic (group INVOICE_VERIFICATION_GROUP). Gate-resolved tracks only. On CREATED also auto-assigns a reviewer.

Auto

Matched documents changed

Adding/removing a reference re-runs the match synchronously via /rematch so the badge + verdicts reflect the completed match, not the pre-match snapshot.

Manual

Run verification

POST …/verification/run requests MatchTrack.all(). A track runs when allowed OR unconfigured; a configured-but-disabled track is dropped.

Manual

Re-run a single track

POST …/rematch?track=ORDER|DELIVERY|PRICE — "Find order / delivery / price-list match" from the detail toolbar.

1.3

The gate — which tracks may run

AutomationGateService.resolveAllowedTracks reads the tenant's active INVOICE automations and filters each by the invoice's current status (empty status list = matches all). A track is allowed iff ≥1 eligible workflow.

Per-track scoping

  • comparePricePerUnit suppression — PRICE is suppressed when every eligible Order (or Price) workflow has "compare price per unit = No"; the pricelist unit price then reads n/a instead of a false green tick.
  • e-delivery scoperesolveEDeliveryOnly reads the delivery workflows' compareEDeliveryNote; under that scope only mobile-channel e-delivery notes match.
  • Ignore service items — SERVICE-type lines are excluded from flagged tracks (serviceLinesSkipped), with an explicit per-line verdict.

Scope precedence (config)

  • A newer invoice__matching_config (per companyAccount, scoped by tenant + project + supplier) supersedes the old INVOICE_VERIFICATION_* automations.
  • In-scope ⇒ all tracks enabled; out-of-scope ⇒ legacy automation fallback.
  • Empty tenant-project assignments = applies to all tenants. Missing tenant/status ⇒ fallback allow-all.

Silent no-op. A "Re-run all" from an unconfigured tenant goes through the gate and can return an empty track set — nothing runs, matchSummary.skipped=true with a reason, and kepler shows a snackbar instead of a false all-green.

1.4

The checks engine

Each check is a Spring bean auto-registered via List<VerificationCheck>. Contract: missing input ⇒ skipped, never an empty flux or a throw. A critical fail blocks readiness; a warning is approve-with-reason.

checkIdClassCategorySeverity
STR_MANDATORY_FIELDSMandatoryFieldsCheckStructuralwarning
FIN_NET_TAX_GROSSNetTaxGrossCheckFinancialwarning
FIN_LINE_SUMLineSumCheckFinancialwarning
TAX_RATE_ALLOWEDTaxRateAllowedCheckTaxwarning
TAX_VATID_FORMATVatIdFormatCheckTaxwarning
MD_IBAN_CHECKSUMIbanChecksumCheckMaster datacritical
MD_BIC_FORMATBicFormatCheckMaster datawarning
MD_BANK_CHANGESupplierBankChangeCheckMaster datacritical
DUP_INVOICEDuplicateInvoiceCheckDuplicatecritical
TMP_INVOICE_DATEInvoiceDateCheckTemporalwarning
TMP_DUE_DATEDueDateCheckTemporalinfo
MATCH_3WAY_QTYsynthesized in the report serviceMatchcritical

Outcome rollup. criticalFailures > 0 or over-consumption ⇒ report FAILED; warnings or a plain mismatch / no-reference ⇒ PASSED_WITH_WARNINGS; a gate-skipped match also maps to PASSED_WITH_WARNINGS; clean ⇒ PASSED. Each run is written to an append-only VerificationReportEntity and logged as a VERIFICATION_RUN Timeline activity — never updated in place.

1.5

Then n8n, now native

Verification used to be three n8n webhook workflows the frontend called and waited on. The v10 engine pulled all of that in-process — same output shape (documentPipelines), but reactive, gated, audited, fast.

Before · n8n

Webhook workflows

Invoice-Price · Invoice-Order · Order-price verification

  • ·Kepler POSTs a webhook and waits — up to an 8-minute HTTP timeout.
  • ·Each track its own workflow, reading Mongo, Typesense catalog & a Postgres query.
  • ·Code nodes computed the match; HTTP calls wrote back the verdicts.
  • ·A "final invoice evaluation" sub-workflow stitched the tracks.
  • ·No audit trail, no per-tenant gating beyond "workflow active", no auto-approve.
Now · altair v10

In-process engine

InvoiceMatchingService + checks, reactive WebFlux, no external hop

  • Kafka auto-trigger + synchronous REST — runs in-request.
  • All three tracks in one engine reading Mongo directly; consumption ledger stops double-billing.
  • Per-tenant gate (tenant + project + supplier + status).
  • Every run frozen into an append-only report + Timeline activity.
  • 11-check engine, unit-aware reconciliation, opt-in touchless auto-approve.
Legacy n8n workflowNative equivalent
Invoice - Price verificationresolvePricelistMatch — assigned, active price lists only
Invoice - Order automationresolveOrderMatch + order__line_consumption ledger
Delivery matchingresolveDeliveryMatch — cross-supplier, unit-aware
pipeline write-back HTTP nodespersistVerdicts — same documentPipelines shape
"final invoice evaluation" sub-workflowChecks engine + VerificationReportEntity