186 lines
5.4 KiB
Python
186 lines
5.4 KiB
Python
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
from src.ingest._common import _reader_raw, _upsert
|
|
from src.ingest.transactions import consolidate_transactions, map_b4w_to_transaction
|
|
from src.models.b4w import B4wDb, B4wExport, Category, CategoryDb
|
|
|
|
|
|
def reader_b4w(path: Path) -> list[B4wExport]:
|
|
raw = _reader_raw(path)
|
|
return list(map(B4wExport.model_validate, raw))
|
|
|
|
|
|
def upsert_category(
|
|
session: Session, category: Category | None, do_commit=True
|
|
) -> CategoryDb | None:
|
|
if category is None:
|
|
return None
|
|
parent = upsert_category(session, category.parent, do_commit=do_commit)
|
|
parent_id = parent.id if parent else None
|
|
matching_rows = session.query(CategoryDb).filter_by(
|
|
name=category.name, parent_id=parent_id
|
|
)
|
|
active = matching_rows.filter(CategoryDb.valid_to.is_(None)).one_or_none()
|
|
if active is not None:
|
|
return active
|
|
|
|
parameters = {"name": category.name, "parent": parent}
|
|
parameters_existing = {**parameters, "valid_to": None}
|
|
parameters_new: dict[str, Any] = {
|
|
**parameters,
|
|
"valid_from": None,
|
|
"valid_to": None,
|
|
"last_action": "created",
|
|
}
|
|
historical = matching_rows.filter(CategoryDb.valid_to.is_not(None)).first()
|
|
if historical is not None:
|
|
parameters_new["valid_from"] = _utc_datetime(None)
|
|
parameters_new["last_action"] = "changed"
|
|
|
|
return _upsert(
|
|
session,
|
|
CategoryDb,
|
|
parameters_existing,
|
|
parameters_new,
|
|
do_update=False,
|
|
do_commit=do_commit,
|
|
)
|
|
|
|
|
|
def _utc_datetime(value: datetime | None) -> datetime:
|
|
if value is None:
|
|
return datetime.now(UTC)
|
|
if value.tzinfo is None or value.utcoffset() is None:
|
|
return value.replace(tzinfo=UTC)
|
|
return value.astimezone(UTC)
|
|
|
|
|
|
def _ensure_after_valid_from(category: CategoryDb, effective_at: datetime) -> None:
|
|
if category.valid_from is None:
|
|
return
|
|
valid_from = _utc_datetime(category.valid_from)
|
|
if effective_at <= valid_from:
|
|
raise ValueError("The action time must be later than valid_from")
|
|
|
|
|
|
def change_category(
|
|
session: Session,
|
|
category_id: int,
|
|
replacement: Category,
|
|
effective_at: datetime | None = None,
|
|
do_commit: bool = True,
|
|
) -> CategoryDb:
|
|
"""Close an active category version and start its replacement version."""
|
|
current = (
|
|
session.query(CategoryDb).filter_by(id=category_id, valid_to=None).one_or_none()
|
|
)
|
|
if current is None:
|
|
raise ValueError(f"No active category found with id {category_id}")
|
|
|
|
action_time = _utc_datetime(effective_at)
|
|
_ensure_after_valid_from(current, action_time)
|
|
parent = upsert_category(session, replacement.parent, do_commit=False)
|
|
parent_id = parent.id if parent else None
|
|
|
|
if current.name == replacement.name and current.parent_id == parent_id:
|
|
return current
|
|
|
|
replacement_active = (
|
|
session.query(CategoryDb)
|
|
.filter_by(name=replacement.name, parent_id=parent_id, valid_to=None)
|
|
.one_or_none()
|
|
)
|
|
|
|
current.valid_to = action_time
|
|
current.last_action = "changed"
|
|
session.flush()
|
|
|
|
if replacement_active is None:
|
|
replacement_active = CategoryDb(
|
|
name=replacement.name,
|
|
parent=parent,
|
|
valid_from=action_time,
|
|
valid_to=None,
|
|
last_action="changed",
|
|
)
|
|
session.add(replacement_active)
|
|
session.flush()
|
|
|
|
if do_commit:
|
|
session.commit()
|
|
return replacement_active
|
|
|
|
|
|
def retire_category(
|
|
session: Session,
|
|
category_id: int,
|
|
effective_at: datetime | None = None,
|
|
do_commit: bool = True,
|
|
) -> CategoryDb:
|
|
"""Close an active category version without creating a replacement."""
|
|
category = (
|
|
session.query(CategoryDb).filter_by(id=category_id, valid_to=None).one_or_none()
|
|
)
|
|
if category is None:
|
|
raise ValueError(f"No active category found with id {category_id}")
|
|
|
|
action_time = _utc_datetime(effective_at)
|
|
_ensure_after_valid_from(category, action_time)
|
|
category.valid_to = action_time
|
|
category.last_action = "retired"
|
|
session.flush()
|
|
if do_commit:
|
|
session.commit()
|
|
return category
|
|
|
|
|
|
def upsert_b4w(
|
|
session: Session, data: B4wExport, do_update=False, do_commit=True
|
|
) -> B4wDb | None:
|
|
category_db = upsert_category(session, data.category)
|
|
|
|
parameters_existing = {"hash_id": data.hash_id}
|
|
|
|
parameters_new = {
|
|
"id_b4w": data.id,
|
|
"hash_id": data.hash_id,
|
|
"account_iban": data.account_iban,
|
|
"account_currency": data.account_currency.value,
|
|
"date_valid": data.date_valid,
|
|
"date_book": data.date_book,
|
|
"amount": data.amount_signed,
|
|
"amount_currency": data.amount_currency.value,
|
|
"text": data.reference,
|
|
"other_name": data.other_name,
|
|
"other_iban": data.other_iban,
|
|
"category": category_db,
|
|
"notes": data.notes,
|
|
}
|
|
|
|
return _upsert(
|
|
session,
|
|
B4wDb,
|
|
parameters_existing,
|
|
parameters_new,
|
|
do_update=do_update,
|
|
do_commit=do_commit,
|
|
)
|
|
|
|
|
|
def ingest_b4w(path: Path, session: Session):
|
|
data = reader_b4w(path)
|
|
for row in data:
|
|
upsert_b4w(session, row, do_commit=False)
|
|
consolidate_transactions(
|
|
session,
|
|
source="b4w",
|
|
StagingDb=B4wDb,
|
|
mapper=map_b4w_to_transaction,
|
|
do_commit=False,
|
|
)
|
|
session.commit()
|