An automated accounts-payable pipeline on Azure: supplier invoices are captured, read with Azure AI Document Intelligence, reconciled against purchase orders and goods receipts by a three-way match, and either posted straight to the ERP or held in a review queue for a human. Everything inside tolerance flows through without manual typing; only exceptions surface.
Reference implementation. This is real, working engineering written to demonstrate the architecture — not a client deliverable. The customer scenario and all figures are illustrative; the code, tests, and infrastructure are not. Processing live documents needs your own Azure resources (see Configuration).
mailbox / upload
│ (drops a PDF in blob storage)
▼
┌────────────────┐ Service Bus ┌────────────────────┐
│ CaptureFunction │ ───────────────▶ │ ProcessInvoiceFunc │
│ (blob trigger) │ invoices-in │ (queue trigger) │
└────────────────┘ └─────────┬──────────┘
│ extract (Document Intelligence)
▼
┌────────────────────┐
│ ThreeWayMatcher │ invoice × PO × receipts
└─────────┬──────────┘
auto-post ◀──┴──▶ exception queue (Azure SQL)
│ │
ERP API LedgerFlow.Api ◀── React review UI
The three-way match is the heart of the system and lives in
LedgerFlow.Core — pure, dependency-free, and
covered by unit tests. Every Azure boundary
(document extraction, messaging, ERP, reference data) sits behind an interface so the domain stays
testable and the cloud stays out of the core.
| Project | Role |
|---|---|
LedgerFlow.Core |
Domain model + three-way matcher and tolerance rules. No I/O. |
LedgerFlow.Infrastructure |
EF Core (Azure SQL), Document Intelligence extractor, Service Bus, ERP + reference-data clients. |
LedgerFlow.Functions |
Isolated-worker Functions: blob capture → Service Bus → extract → match → post. |
LedgerFlow.Api |
Minimal API backing the exception queue (list / approve / reject) + /api/analytics aggregates. |
web/ |
React + TypeScript + Vite review-queue UI. |
infra/ |
Bicep for the whole estate; managed identity throughout, no secrets in app settings. |
Prerequisites, a clean-machine setup and troubleshooting are in
INSTALL.md. Short version:
dotnet test LedgerFlow.sln # matcher + per-supplier policy + pipeline orchestration tests
cd web && npm ci && npm run buildThe pipeline orchestration (extract → match → post-or-queue, including duplicate suppression) is covered end-to-end in InvoiceProcessorTests against an in-memory database and a fake ERP. Per-supplier tolerance overrides live in SupplierPolicies — a strategic partner can carry a looser price band while a repeat over-biller gets zero headroom.
Both run in CI on every push, along with az bicep build over the
infrastructure.
docker compose up -d # SQL Server + Azurite
dotnet run --project src/LedgerFlow.Api # exception-queue API on :5080
cd web && npm run dev # review UI on :5173, proxied to the APIThe Functions worker and live extraction need real Azure resources — see below.
LedgerFlow.Functions reads (via managed identity in Azure, local.settings.json locally):
| Setting | Purpose |
|---|---|
DocumentIntelligenceEndpoint |
Azure AI Document Intelligence resource URL |
ServiceBusConnection |
Service Bus namespace |
BlobServiceUri |
Storage account for the invoice inbox |
SqlConnectionString |
Azure SQL exception-queue database |
ErpBaseUrl |
ERP posting + reference-data API |
Provision everything with az deployment group create -g <rg> -f infra/main.bicep.
Design and code by Nick Zivkovic.