from __future__ import annotations import csv from datetime import date from decimal import Decimal from pathlib import Path from typing import Any def read_transaction_csv( path: Path, *, household_id: str, source_type: str, source_account_id: str | None, owner_user_id: str | None, ) -> list[dict[str, Any]]: rows: list[dict[str, Any]] = [] with path.open(newline="", encoding="utf-8-sig") as handle: reader = csv.DictReader(handle) for row in reader: event_date = _pick( row, "event_date", "date", "booking_date", "Buchungstag", "Valutadatum" ) amount = _pick(row, "amount", "Betrag", "value") if not event_date or not amount: continue merchant = _pick( row, "merchant", "counterparty", "Auftraggeber/Empfänger", "Name Zahlungsbeteiligter", ) description = _pick(row, "description", "purpose", "Verwendungszweck", "Buchungstext") currency = _pick(row, "currency", "Waehrung", "Währung") or "EUR" provider_transaction_id = _pick( row, "provider_transaction_id", "transaction_id", "Umsatz ID" ) normalized = { "household_id": household_id, "source_type": source_type, "source_account_id": source_account_id, "owner_user_id": owner_user_id, "source_reference": provider_transaction_id, "provider_transaction_id": provider_transaction_id, "raw_payload_json": row, "amount": str(_parse_amount(amount)), "currency": currency, "event_date": _parse_date(event_date).isoformat(), "merchant": merchant, "description": description, "event_type": "expense" if _parse_amount(amount) < 0 else "income", "status": "confirmed", } normalized["amount"] = str(abs(_parse_amount(amount))) rows.append(normalized) return rows def _pick(row: dict[str, str], *keys: str) -> str | None: lowered = {key.lower(): value for key, value in row.items()} for key in keys: value = row.get(key) or lowered.get(key.lower()) if value: return value.strip() return None def _parse_amount(value: str) -> Decimal: normalized = value.strip().replace(".", "").replace(",", ".") return Decimal(normalized) def _parse_date(value: str) -> date: stripped = value.strip() for fmt in ("%Y-%m-%d", "%d.%m.%Y", "%d/%m/%Y"): try: return ( date.fromisoformat(stripped) if fmt == "%Y-%m-%d" else __import__("datetime").datetime.strptime(stripped, fmt).date() ) except ValueError: continue raise ValueError(f"Unsupported date format: {value}")