dodil/erp-brick-expenses
DODIL ERP brick: expenses
src/brick_expenses/infrastructure/readers.py
"""The receipt reader (EXP-01, ai_steps.read_receipt): a PORT, with no implementation shipped.
GAP (recorded): erp-platform's AI capabilities (capabilities.yaml ai.suggest / ai.draft_message / ai.flag_risk) take a context of JSON scalars, not an
image, so none of them can read a receipt photo; and no sovereign vision model is reachable from a brick yet (every image model was down when the
platform last checked). The step is built to its contract - consent first, suggest mode only, every field marked until the person confirms it, the
audit row, the fallback of empty fields - against this port. A composition (or the platform, once there is a model) sets `ports.reader`; with none set the
fallback is what happens: the image is stored and the employee types the fields.
`read(image, content_type, categories)` returns {"model": "<name>", "fields": {date, merchant, total, currency, vat, vat_number, category_code,
category_reason}, "confidence": {field: 0..100}} - suggestions only: nothing here is ever confirmed by the reader.
"""
from typing import Optional, Protocol
class ReaderDown(Exception):
"""The model could not answer: the step falls back to empty fields (never a refusal of the upload)."""
def __init__(self, reason: str):
super().__init__(reason)
self.reason = reason
class ReceiptReader(Protocol):
def read(self, image: bytes, content_type: str, categories: list) -> dict: ...
class Ports:
def __init__(self):
self.reader: Optional[ReceiptReader] = None
ports = Ports()