Largely vibe coded.
A production-minded MVP Kotlin backend for scanning invoice/receipt emails and identifying possible Finnish tax-deduction candidates using local Ollama models and semantic search with pgvector.
- SourceDocument: A deduplicated raw imported file (email body, PDF, or text file)
- DocumentChunk: A sliding-window text chunk with embedded semantic vector
- Embedding: Vector representation from Ollama embed model
- InvoiceExtraction: Structured invoice fields extracted by LLM
- DeductionCandidate: A possible tax deduction with evidence, confidence, and missing information
- TaxRuleChunk: Embedded chunks from Finnish tax rule knowledge base
1. Import fixture emails/PDFs/text files
↓
2. Extract text from attachments and bodies
↓
3. Deduplicate by SHA-256 hash
↓
4. Create SourceDocument records
↓
5. Chunk text using sliding windows
↓
6. Generate embeddings via Ollama (mxbai-embed-large)
↓
7. Store chunks and embeddings in PostgreSQL + pgvector
↓
8. Extract structured invoice fields via LLM (llama3)
↓
9. Retrieve relevant tax-rule chunks via semantic search
↓
10. Classify into possible deduction candidates
↓
11. Store candidates with evidence and confidence scores
- Language: Kotlin with Spring Boot 3.3.4
- Database: PostgreSQL 16 with pgvector extension
- ORM/Query: JDBC with explicit SQL (Spring Data JDBC not used)
- Vector Search: pgvector with IVFFlat indexes (cosine similarity)
- Embedding Model: Ollama
mxbai-embed-large(1024-dimensional) - Chat Model: Ollama
llama3(for extraction and classification) - PDF Parsing: Apache PDFBox 3.0.2
- Email Parsing: Jakarta Mail 2.0.1
- Testing: Testcontainers, JUnit Jupiter, MockK, AssertJ
- Docker and Docker Compose
- Java 21+
- Ollama running locally with
mxbai-embed-largeandllama3models
# Download Ollama from https://ollama.ai
# Or via package manager (e.g., Homebrew)
ollama pull mxbai-embed-large
ollama pull llama3
# Start Ollama server (default port 11434)
ollama servedocker-compose up -dVerify:
docker exec -it verotin-postgres psql -U verotin -d verotin -c "SELECT * FROM source_documents;"gradle buildWithout ingesting fixtures:
SPRING_PROFILES_ACTIVE=default gradle bootRunOr as a JAR:
gradle bootJar
java -jar build/libs/verotin-0.0.1-SNAPSHOT.jarServer runs on http://localhost:8080
Automatically ingest sample emails and classify them on startup:
SPRING_PROFILES_ACTIVE=dev gradle bootRunWatch logs:
[INFO] [...] Loading fixture files...
[INFO] [...] Persisted SourceDocument id=... filename=sample_invoice_1.eml
[INFO] [...] Stored 5 document chunks (5 with embeddings)
[INFO] [...] Extracted invoice from ...: vendor=TechOffice Oy, amount=253.55
[INFO] [...] Classified invoice ... into 1 deduction candidates
[INFO] [...] Fixture ingest complete
GET /api/documents— List all source documentsGET /api/documents/{id}— Get document by IDGET /api/documents/{id}/chunks— Get chunks for a document
GET /api/extractions— List all invoice extractionsGET /api/extractions/{id}— Get extraction by IDGET /api/extractions/source/{sourceDocId}— Get extraction for a documentGET /api/extractions/unclassified— Get extractions without deduction candidates
GET /api/deductions— List all candidatesGET /api/deductions/{id}— Get candidate by IDGET /api/deductions/extraction/{extractionId}— Get candidates for an extractionGET /api/deductions/pending— Get pending candidates (awaiting review)GET /api/deductions/category/{category}— Get candidates by categoryPOST /api/deductions/{id}/accept— Mark candidate as acceptedPOST /api/deductions/{id}/reject— Mark candidate as rejected
GET /api/search/documents?query=...&topK=5— Semantic search documentsGET /api/search/tax-rules?query=...&topK=5— Semantic search tax rules
GET /actuator/health— Health check (database, Ollama connectivity)GET /api/stats— System statistics (document count, chunk count, deduction summary)
Fixture files already include samples. To add a new email:
- Copy
.emlfile tosrc/main/resources/fixtures/ - Re-run with dev profile:
SPRING_PROFILES_ACTIVE=dev gradle bootRun
curl http://localhost:8080/api/deductions/pendingResponse:
[
{
"id": "...",
"invoiceExtractionId": "...",
"category": "tyohuonevahennys",
"deductibleAmount": 47.99,
"confidence": 0.72,
"justification": "Office supplies (keyboard, lamp) are used for home office setup. ...",
"missingInformation": "Verification of home office space percentage; employment status confirmation",
"suggestedNextAction": "Confirm home office percentage and employment contract status",
"evidenceSnippets": [
"Ergonomic keyboard: €89.99",
"Desk lamp LED: €34.50",
"This invoice is for work-related office equipment and supplies."
],
"status": "PENDING",
"createdAt": "2024-02-15T14:30:00Z"
}
]curl -X POST http://localhost:8080/api/deductions/{candidateId}/acceptcurl "http://localhost:8080/api/search/documents?query=home%20office%20equipment&topK=3"Response:
{
"query": "home office equipment",
"topK": 3,
"results": [
"This invoice is for work-related office equipment and supplies.",
"Ergonomic keyboard: €89.99 (qty: 1)"
]
}gradle testCurrently includes:
ChunkingServiceTest: Sliding window chunking logic
gradle integrationTestUses Testcontainers to spin up PostgreSQL for each test suite.
gradle test --tests ChunkingServiceTestEnsure Ollama is running:
curl http://localhost:11434/api/tagsIf not:
ollama serveCheck Docker container:
docker ps | grep verotin-postgres
docker logs verotin-postgresRestart if needed:
docker-compose down
docker-compose up -dPull the required models:
ollama pull mxbai-embed-large
ollama pull llama3
ollama listEnsure PostgreSQL is running (Gradle test framework will use Testcontainers):
gradle build -x test- Gmail/Outlook OAuth for live email ingestion
- Async job queue for bulk processing
- Fine-tuned Finnish tax LLM
- UI dashboard for candidate review
- Tax year configuration and rule versioning
- Batch PDF invoice scanning from folders
- Multi-language support
- Confidence calibration metrics
This system does NOT provide final tax advice. It identifies possible candidates that require human verification. All candidates are marked as "PENDING" and must be reviewed by a qualified tax professional before being used in any tax filing. The developers and maintainers assume no liability for classifications or outcomes of tax filings based on this system.
Edit src/main/resources/application.yml:
spring:
datasource:
url: jdbc:postgresql://localhost:5432/verotin
username: verotin
password: verotin
flyway:
enabled: true
ollama:
base-url: http://localhost:11434
embed-model: mxbai-embed-large
chat-model: llama3
embedding-dimensions: 1024- Create data class in
domain/ - Create repository with explicit SQL in
repository/ - Write tests in
src/test/kotlin/ - Add migration in
db/migration/ - Wire into service layer in
service/
Defaults: 1500-character windows with 200-character overlap. Adjust in src/main/resources/application.yml or via constructor in ChunkingService.
Currently 1024 (mxbai-embed-large). If changing models, create new migration to alter column size and rebuild indexes.