"""In-place monetary-field parsing for the feature-extraction pipeline. When a feature-extract request declares ``monetary_fields`` — an object name plus a list of field names — the extracted ``data`` payload is post-processed so those fields become real numbers instead of raw strings, and a per-field status report is returned so callers can see exactly what happened to each declared field. Example ------- Request config:: {"object_name": "invoice", "field_names": ["total", "invoice_no"]} Extracted data:: {"invoice": [{"number": "INV-2026-00125", "total": "1250.75"}]} After :func:`apply_monetary_fields` the same dict has ``"total": 1250.75`` (price-parsed) and the returned report explains each field: * ``total`` -> status ``parsed``, parsed_value 1250.75 * ``invoice_no`` -> status ``not_found`` (no such field in the object) """ from __future__ import annotations from typing import Any, List, Optional from pydantic import BaseModel, Field from app.services.price_parser import parse_amount class MonetaryFieldStatus(BaseModel): """Outcome of one declared monetary field, per extracted object item.""" object_name: str """Container key the field was looked up in (from the request config).""" field: str """Monetary field name (from the request config).""" item_index: int = 0 """Index of the object inside the container this report refers to.""" status: str = Field( default="not_found", description="One of: parsed, not_found, not_parsable, skipped.", ) value: Optional[str] = None """The raw value as extracted (string form), when present.""" parsed_value: Optional[float] = None """Parsed numeric amount when status == 'parsed'.""" error: Optional[str] = None """Human-readable reason when the field was not parsed.""" def _status(object_name: str, field: str, item_index: int, **kw) -> MonetaryFieldStatus: return MonetaryFieldStatus(object_name=object_name, field=field, item_index=item_index, **kw) def apply_monetary_fields(data: Any, object_name: str, field_names: List[str]) -> List[MonetaryFieldStatus]: """Price-parse the configured monetary fields inside ``data``, in place. ``object_name`` selects the container key in ``data`` (e.g. ``"invoice"``); its value may be a list of objects or a single object. For every listed ``field_names`` entry inside each object: * value missing -> status ``not_found`` (error ``field 'x' not found``) * value ``None`` / blank -> status ``skipped`` (error ``value is empty``) * value that does not parse -> status ``not_parsable`` with the raw value * value that parses -> replaced with the numeric amount, status ``parsed`` Returns one :class:`MonetaryFieldStatus` per (object item, field). """ report: List[MonetaryFieldStatus] = [] if not isinstance(data, dict) or not object_name: for field in field_names: report.append( _status( object_name, field, 0, status="not_found", error=f"object '{object_name}' not found in extracted data", ) ) return report container = data.get(object_name) if isinstance(container, list): items: List[Any] = container elif isinstance(container, dict): items = [container] else: for field in field_names: report.append( _status( object_name, field, 0, status="not_found", error=f"object '{object_name}' not found in extracted data", ) ) return report for idx, item in enumerate(items): if not isinstance(item, dict): for field in field_names: report.append( _status( object_name, field, idx, status="not_found", error="item is not an object", ) ) continue for field in field_names: if field not in item: report.append( _status( object_name, field, idx, status="not_found", error=f"field '{field}' not found", ) ) continue value = item[field] if value is None or (isinstance(value, str) and not value.strip()): report.append( _status( object_name, field, idx, status="skipped", error="value is empty", ) ) continue parsed = parse_amount(value) if parsed is None: report.append( _status( object_name, field, idx, status="not_parsable", value=str(value), error=f"value is non-parsable: {value}", ) ) continue item[field] = parsed.amount_float report.append( _status( object_name, field, idx, status="parsed", value=str(value), parsed_value=parsed.amount_float, ) ) return report __all__ = ["MonetaryFieldStatus", "apply_monetary_fields"]