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()