docu-ai-flow
A .NET 10 service that watches a folder, extracts the data from PDF invoices, files them and sends the quarter to your tax advisor. Two ideas carry it: extraction lives behind a single port — local templates today, Google Document AI yesterday — and no invoice is filed unseen until its supplier has earned it. Two commands to run, and not one credential.
The problem
Managing supplier invoices is repetitive manual work: downloading PDFs, opening them one by one, copying data into a spreadsheet, sending everything to the advisor each quarter. One mistake at any step means incorrect tax filings. But full automation has a trap of its own: no extraction is right 100% of the time, and a misread figure nobody looks at ends up in a filing.
The solution
-
Folder watcher
Detects PDFs in ./data/inbox/ in real time. A filesystem watcher plus a polling fallback guarantee no invoice is ever missed. Duplicates are caught by content hash and by natural key (number + tax id) — the latter is what catches a re-sent invoice whose bytes differ, before the same VAT is declared twice.
-
Swappable extraction
One port, IInvoiceDataExtractor, with two live adapters: local templates over PdfPig (the default — no network, no credentials, €0 per invoice) and Google Document AI. Which one is bound is a configuration value, not a code change.
-
OCR for scans
A scan has no text layer, so there is nothing to read. With the fallback on, poppler rasterises the page and Tesseract reads it; the recovered text goes through the same template pipeline. With it off, a Null Object returns an empty string and no branch of the extractor has to ask whether OCR exists.
-
Review, PDF alongside
Invoices that are not filed automatically wait in a queue. The review screen puts the PDF on the left and the extracted fields on the right, each carrying the confidence it was captured with. Anything the extractor could not read becomes a blank form, with its own net + VAT = total check.
-
Excel export
ClosedXML generates a quarterly Excel file with native types — real dates and numbers ready to sort, filter and sum. A cumulative master spreadsheet is regenerated on every confirmed invoice.
-
Advisor dispatch
Resend packages the quarter's PDFs into a ZIP and emails them to the tax advisor. If the ZIP exceeds Resend's 40 MB limit, it splits automatically by month and sends one email per part.
Try it
No clone, no .NET install. The image is published to GHCR from CI for amd64 and arm64, and boots on the local extractor: no accounts, no keys.
- 1 Write eight fictional invoices into the inbox
docker run --rm -v "$PWD/data:/app/data" \ ghcr.io/aitorevi/docu-ai-flow:latest make-samples /app/data/inbox - 2 Start the watcher and the panel
docker run --rm -p 8080:8080 -v "$PWD/data:/app/data" \ ghcr.io/aitorevi/docu-ai-flow:latest
Open localhost:8080. Three of those invoices come from the same supplier: confirm them without changing anything and watch Suministros Aurora cross from "In review" to "Automatic" — from then on its invoices are filed on their own. That is the idea the whole project rests on, and it takes a minute to see.
Tech stack
Backend
- .NET 10
- C#
- Hexagonal
- Result/Error
- SQLite
- PdfPig
- Google Document AI
- Tesseract
- Resend
- Polly
Frontend & deployment
- Astro 6
- TypeScript
- Docker
- GHCR
- GitHub Actions
Tests
- xUnit
- NSubstitute
- WireMock.Net
- NetArchTest
- WebApplicationFactory
Technical decisions
-
Hexagonal architecture
The extractor, SQLite, the file archiver and Resend are swappable adapters behind interfaces (ports). Application use cases know no implementation detail. A NetArchTest architecture test enforces these boundaries on every build.
-
The hexagon, demonstrated
The project started on LlamaParse, moved to Google Document AI when its accuracy fell short, and ended up on a local template extractor that beats both on the invoices it actually sees. Across all three swaps the domain, the use cases and the pipeline tests were untouched: they depend on the port, not the provider. That is the whole return on the hexagon, and it is why the default today is the free one.
-
Trust that is earned
The interesting question is not whether a human reviews, but which invoices earn the right to skip review — and that is decided per supplier, by track record. After N consecutive confirmations in which nobody changed anything, that supplier starts being filed automatically. A single correction resets the counter and revokes it. Autonomy is earned, and it can be lost.
-
Result/Error for business failures
A low-confidence extraction, incoherent total or missing invoice number is an expected business case, not an exception. Result<TValue, TError> expresses it as a return value. Exceptions are reserved for transient failures — API down, disk full — which Polly retries automatically.
-
Runnable with no credentials
A portfolio project you can only read is worth half as much. The image is published to GHCR from CI, ships poppler and Tesseract inside, and boots on the local extractor: two Docker commands, no accounts and no keys, and eight fictional invoices that walk every path — including one from a supplier nobody wrote a template for, and a scan with no text at all.
-
Built with Claude Code agents
Every feature followed the same cycle: the orchestrator explored and debated the approach, the planner designed the architecture, the coder implemented in TDD, and the reviewer audited against the approved plan. The result: consistent code and a commit history that tells the story of every decision.
Built with Claude Code
The project is also an experiment in how to organise AI-assisted development. The agents and skills live in .claude/ and are part of the public repo.
-
Orchestrator
Coordinates the full flow: explore, debate, plan, implement, review. No phase starts without explicit human agreement. The plan lives in workflow/ and moves through plans/ → in-progress/ → reviewing/ → done/.
-
Specialised agents
planner-backend designs the architecture and contracts; coder-backend implements in strict TDD; reviewer audits against the approved plan. Each agent has its own system prompt under .claude/agents/.
-
Reusable skills
Markdown skills loaded at session start: commit.md defines the commit format, tdd.md encodes the Red-Green-Refactor cycle, orchestrate.md the full orchestration flow.
-
AI-assisted TDD
Every feature starts with a failing test. 262 tests across three layers: Domain (pure), Application (use cases with NSubstitute doubles) and Integration — real SQLite and real files, the whole API hosted in-process through WebApplicationFactory, and a pipeline test that walks a supplier from its first review to filing itself.
Interested in the AI workflow?
The repo is public and runs with no credentials. The agents, skills and orchestration flow are in .claude/ alongside the source code. Use them as a template for your own projects.