87 lines
3.0 KiB
Python
87 lines
3.0 KiB
Python
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}")
|