"""Commands and queries for categories and transactions. CQRS: the ``add_*``/``delete_*`` methods are commands (state changes); ``list_*`` methods are queries (read-only). Neither side leaks the other. """ from __future__ import annotations from datetime import date from ..errors import ConflictError, NotFoundError, ValidationError from ..money import Money from ..models import Category, Transaction, clean_name, clean_note, validate_kind from ..repository import Repository __all__ = ["LedgerService"] class LedgerService: """Owns category and transaction rules (R5-R10, R12).""" def __init__(self, repo: Repository) -> None: self._repo = repo # -- commands ---------------------------------------------------------- def add_category(self, name: str, kind: str) -> Category: clean = clean_name(name, "category") validate_kind(kind) if self._repo.find_category(clean) is not None: raise ConflictError(f"category {clean!r} already exists") self._repo.create_category(clean, kind) return Category(name=clean, kind=kind) def delete_category(self, name: str) -> None: clean = clean_name(name, "category") ref = self._repo.find_category(clean) if ref is None: raise NotFoundError(f"unknown category {clean!r}") category_id, stored_name, _kind = ref if self._repo.category_in_use(category_id): raise ConflictError( f"category {stored_name!r} has transactions or a budget" ) self._repo.delete_category(category_id) def add_transaction( self, amount: Money, category: str, day: date, note: str | None = None ) -> Transaction: if not amount.is_positive: raise ValidationError("amount must be greater than zero") ref = self._repo.find_category(category) if ref is None: raise NotFoundError(f"unknown category {category!r}") category_id, stored_name, kind = ref cleaned_note = clean_note(note) tx_id = self._repo.insert_transaction( amount.cents, category_id, day.isoformat(), cleaned_note ) return Transaction( id=tx_id, amount=amount, category=stored_name, kind=kind, date=day, note=cleaned_note, ) def delete_transaction(self, tx_id: int) -> None: if not self._repo.transaction_exists(tx_id): raise NotFoundError(f"unknown transaction #{tx_id}") self._repo.delete_transaction(tx_id) # -- queries ----------------------------------------------------------- def list_categories(self) -> list[Category]: return [Category(name=name, kind=kind) for name, kind in self._repo.list_categories()] def list_transactions( self, start: str | None, end: str | None, category: str | None = None, ) -> list[Transaction]: category_id = None if category is not None: ref = self._repo.find_category(category) if ref is None: raise NotFoundError(f"unknown category {category!r}") category_id = ref[0] rows = self._repo.list_transactions(start, end, category_id) return [ Transaction( id=row["id"], amount=Money(cents=row["amount_cents"]), category=row["category"], kind=row["kind"], date=date.fromisoformat(row["date"]), note=row["note"], ) for row in rows ]