Datalumina

Business logic

Business rules

The Northstar policy as pure Python, separate invoice and receipt rules, offline VAT validation, and the fixed GL catalog.

By the end of this stage the Northstar rulebook exists as ordinary Python. Given normalized document data, the policy produces the same list of errors and warnings every time, with no network call, no database, and no model anywhere near the decision.

This comes straight back to the brief. The client's real question is never "what does the model say", it is "does this document satisfy our policy". Now that you have seen real provider output and its gaps, you can write credible rules against it.

Policy is ordinary code

The whole policy lives in backend/app/invoices/validation.py as pure functions. Documents in, issues out. There is nothing clever about the implementation, and that is the point. A finance rulebook must be readable by the people accountable for it, testable without Azure, and changeable without touching extraction.

Errors block approval. Warnings stay visible but do not block. That severity split is itself policy:

Invoice rules

An invoice at Northstar must clear these checks before approval:

  • Supplier and customer identity are present
  • The supplier VAT number has a valid EU format and checksum
  • The customer VAT number matches Northstar's configured VAT ID
  • Invoice number, invoice date, currency, and a positive total are present
  • The due date does not precede the invoice date
  • Subtotal plus VAT reconciles to the total within EUR 0.01
  • The supplier and invoice number combination is not a duplicate of an earlier upload

A missing purchase order and primary-extraction confidence below 0.80 are warnings. Real documents are messy, and a policy with only hard failures would drown the reviewer.

Receipt rules

A receipt records an expense that was already paid, so demanding invoice fields would be wrong. Receipts require merchant, transaction date, currency, a positive total, and a VAT total. When subtotal and VAT are both present they must reconcile within EUR 0.01. Low confidence is a warning. There is no purchase order, invoice number, customer VAT, or due date requirement.

Two document types, two policies, one normalized data model. This is the design decision the fuel receipt in the corpus exists to force.

VAT validation is offline

VAT format and checksum validation runs locally with python-stdnum, a library that encodes each EU country's number scheme. The app deliberately makes no claim about live registration. Checking that a Dutch VAT number is structurally valid is deterministic and free; checking that it is actively registered would require the EU VIES service, which is an availability and rate-limit dependency this build keeps out of scope. The distinction is stated honestly in the UI rather than blurred.

The GL catalog

Northstar's chart of accounts is a fixed catalog in backend/app/accounting/catalog.py. The model may suggest an account, but the suggestion is validated against the catalog before anyone sees it, an invalid or hallucinated account code fails loudly. Approval requires a valid selected account, and Maya can always override the suggestion. The same pattern covers correction emails, where deterministic eligibility rules decide whether a supplier-fixable issue exists before the LLM is allowed to draft any text.

Lint the policy modules to close the stage:

cd backend
uv run --locked --no-sync ruff check \
  app/invoices/validation.py \
  app/accounting \
  app/correction_email

Checkpoint

  • Given normalized data and a duplicate flag, the policy result is reproducible offline
  • You can name which invoice checks are errors and which are warnings, and why receipts differ
  • You can explain what the app claims about VAT, and what it deliberately does not claim

Next: The hybrid pipeline

Wire classification, both extractions, the deterministic merge, and the rules into one chain.

On this page