Invoice Intake Automation · AfifAI
Case study · Finance automation · 2026

Supplier invoices that read, check and route themselves

An Israeli non-profit that runs several residential centers was keying every supplier invoice into its ERP by hand. I led the build of a platform that reads scanned invoices with Azure AI Document Intelligence, runs 14 automatic checks on every one, and sends people only the invoices that actually need a human decision.

.NET 8EF Core / SQL ServerAzure AI Document IntelligenceReact 18Ant DesignPriority ERPBitbucket Pipelines
Client
Israeli non-profit running residential centers (confidential)
My role
Team lead & architect (hands-on)
Timeline
Phase 1 in 4 weeks · Aug–Sep 2026
Status
Phase 1 · 7 of 8 milestones
At a glance
≈6 to ≈1 minhands-on time per clean invoice: keying replaced by a quick reviewestimate
14automatic checks on every invoice, each mapped to a named exception reason
1 to every fieldconfidence checked per critical field, not one document score that read 1.0 on a misread invoice
3 / 3lines recovered from a dense table the OCR had merged into one, accepted only if they sum exactly
28 daysfrom empty repos to Phase 1: 7 of 8 milestones, 164 commits, 46 feature branches
167automated tests, run with the build on every push
The problem

Every invoice typed in, every check done by eye

Suppliers send tax invoices, consolidated invoices, delivery notes and credit notes, often as phone scans or emailed PDFs and sometimes handwritten. Before anything could be entered into the ERP, someone had to read each one, check that it was addressed to the organization, work out which center it belonged to, check the arithmetic, look for a stamp and signature, and make sure it had not already been entered. The work was slow and repetitive, and mistakes were easy to miss.

What I owned

Architecture to working system

  • Designed the modular-monolith architecture from the approved low-level design and led delivery across 3 repositories (API and worker, web app, infrastructure).
  • Wrote most of the code myself: intake, the queue, the extraction adapter, matching, the validation engine, exception handling and the review UI.
  • Tuned extraction against real customer invoices over 7 feedback rounds, turning each failure into a rule or a safe fallback.
  • Ran the open-questions log with the customer and turned business answers (exact totals, center aliases, confidence policy) into configuration.
  • Set up CI, one-command database install, backup and restore, and the audit log the security team required.
Architecture

One deployable, clear module boundaries, and a queue in between

The diagram is wider than the screen: swipe sideways to see all of it →

The ERP is reached only through one provider (an anti-corruption layer). In Phase 1 it exposes read-only lookups, and every provider without configuration fails closed, so nothing ever pretends an integration answered. The queue sits behind an interface: SQL-backed for now, Azure Service Bus later through DI, with no change to business code.

Design decisions

OCR suggests. Deterministic checks decide.

Exact-sum gates around the model

The OCR model is good but not trustworthy on its own. Anything the system repairs, such as table rows it rebuilds or reference lines it drops, is accepted only when the result sums exactly to the printed total. Trade-off: more invoices go to a person early on, but nothing unverified gets in.

Exact matching, no fuzzy guesses

A center, SKU or budget code resolves only when exactly one candidate matches. Zero matches means Not found and several means Ambiguous, and both send the invoice to review. Reviewer edits are never overwritten. Trade-off: lower automation until the real mapping data is loaded.

A SQL-backed queue behind an interface

The Azure environment wasn't decided yet, so the queue runs on SQL Server with an atomic claim, retries, backoff and dead-letters, behind IProcessingQueue. Handlers are idempotent. Trade-off: it polls every 5 seconds, which is fine at this volume.

The same auth plumbing from day one

The customer postponed Entra ID. Instead of leaving the API anonymous, it issues short-lived JWTs from a simulated sign-in, using the same claims, guards and audit identity. Trade-off: switching to Entra ID replaces only the token issuer.

Engineering log

What real invoices taught me

Tests with real customer documents came early and often. Each problem below came from a real invoice.

Sep 1

Two workers, one message

A read-then-update claim lets two consumers pick up the same queued document.

Claimed messages with a single SQL statement that updates one row and returns it, so there is no gap for a race. Messages stuck in processing after 10 minutes are recovered, and after 3 attempts they go to a dead-letter list that operators can requeue.

Sep 2

Confidence 1.0, amounts wrong

On a handwritten invoice the service read 28,500 as 2,800 and 5,130 as 130, with field confidence 0.98 and document confidence 1.0.

Reported confidence is now the minimum across all critical header and line fields. The arithmetic cross-checks (lines → subtotal → VAT → total, with zero tolerance) became the main safety net, and they caught this invoice.

Sep 2

Three products in one line

On dense printed tables the model merged rows or shifted quantities and prices between lines.

When the model's lines don't add up, the system rebuilds them from the recognised table grid. The rebuild is kept only if it sums exactly. Reviewers can also add, remove and split lines, and the original reading is kept unchanged.

Sep 3

A correct tax ID flagged as unreadable

A tax ID in small print was read correctly, but at 0.54 confidence, below the 95% threshold. Without a fix, most scanned documents would have failed on this one field.

Israeli company and personal IDs carry a check digit. An ID that passes the checksum is treated as verified and left out of the confidence minimum. The customer approved this as a recorded exception to the threshold policy.

Sep 3

The invoice was on page 3

The development tier analyses 2 pages per call. In one file the delivery note on pages 1–2 had the lines, and the tax invoice came after them.

Later pages are analysed too, and only the missing identification fields are taken from them. Amounts stay with the primary result, and if the extra page was read with lower confidence, that lower confidence is the one reported.

Sep 3

Delivery-note number read as the invoice number

The model took the number printed next to the delivery-note title as the invoice number.

That number now goes into dedicated delivery-note fields, and the invoice number is left empty. The required-field rule then asks the reviewer for the real one. Credit notes are handled the same way.

Sep 6

Several invoices in one file

A single upload sometimes held more than one invoice.

The file is split into separate documents in the same process, but only when each part has its own invoice number and payable total. Weaker candidates stay attached to the main invoice.

Sep 6

An approval hidden in an email thread

Some scans were printed email threads where a center manager had replied “approved”.

A conservative, deterministic detector looks for email-thread markers plus an approval word on a line of its own (“not approved” never matches). It shows up as a hint for the reviewer and never approves anything automatically.

Follow one invoice

From upload to approval

A walkthrough with a synthetic invoice: supplier, center and amounts are invented. Choose a stage to see what the system does and which safeguard applies.

Outcome

Where it landed

AreaBeforeAfter
Data entryEvery field typed by handExtracted automatically. People review only flagged invoices (≈6 → ≈1 min each, estimate)
ChecksBy eye, and it varied by person14 rules on every invoice, with 12 named exception reasons
ConfidenceOne document score (1.0 on a misread invoice)Lowest score across every critical field, plus exact arithmetic checks
ApprovalsOne document at a timeMulti-select bulk approval, with the approver's identity and time recorded
Multi-invoice filesSeparated by handSplit automatically into separate documents, each validated on its own
FailuresSilent3 retries with backoff, a dead-letter list with requeue, and a daily report
Database setupManual migration stepsOne-command install, plus backup and restore scripts

Phase 1 covers intake, extraction, matching, validation, review and approval, tested on real customer documents. The ERP read lookups (M7) are waiting on the ERP vendor's API details. Posting to the ERP and mailbox intake are planned for the next phases.

Stack

Technology stack

Backend
.NET 8, ASP.NET Core, modular monolith, BackgroundService worker
Data
EF Core 8, SQL Server, code-first migrations, soft delete + audit
Document AI
Azure AI Document Intelligence (prebuilt invoice), custom mapper
Front end
React 18, TypeScript, Redux Toolkit, Ant Design, i18next (Hebrew RTL)
Integration
Priority ERP provider (anti-corruption layer), Entra ID-ready JWT
Ops
NLog, SIEM JSON audit log, correlation IDs, Bitbucket Pipelines, xUnit

Free scoping call · 30 minutes · No commitment

Bring one process. Leave with a plan and a number to beat.

Write a few lines: the process or system, what hurts today (time, errors, cost or an AI pilot that stalled) and who uses it. After a short call you get a written plan and a fixed quote within ≈3 days.

Get a free scoping callafif.mansour@gmail.com

Opens a pre-filled email · reply within one business day

The model reads. The code counts.

All projects