first ruggedy draft
This commit is contained in:
86
apps/local-agent/mousehold_agent/csv_importer.py
Normal file
86
apps/local-agent/mousehold_agent/csv_importer.py
Normal file
@@ -0,0 +1,86 @@
|
||||
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}")
|
||||
Reference in New Issue
Block a user