"""Budgets und Budgetvorlagen.""" from datetime import date from fastapi import APIRouter, Query, status from sqlalchemy import select from sqlalchemy.exc import IntegrityError from app.api.deps import DbSession from app.core.errors import ConflictError, ValidationError from app.models import Budget, BudgetTemplate, Category from app.schemas.budget import ( BudgetCreate, BudgetOut, BudgetTemplateCreate, BudgetTemplateOut, BudgetTemplateUpdate, BudgetUpdate, ) from app.schemas.common import ErrorResponse, MessageResponse from app.services.crud import apply_updates, get_or_404 router = APIRouter(prefix="/budgets", tags=["budgets"]) NOT_FOUND = {status.HTTP_404_NOT_FOUND: {"model": ErrorResponse}} @router.get( "", response_model=list[BudgetOut], summary="Budgets auflisten", description="Ohne `month` werden alle Monate geliefert.", ) async def list_budgets( session: DbSession, month: date | None = Query(default=None, description="Beliebiger Tag im gesuchten Monat."), category_id: int | None = Query(default=None), ) -> list[Budget]: stmt = select(Budget).order_by(Budget.period_month.desc(), Budget.category_id) if month is not None: stmt = stmt.where(Budget.period_month == month.replace(day=1)) if category_id is not None: stmt = stmt.where(Budget.category_id == category_id) return list((await session.execute(stmt)).scalars().all()) @router.post( "", response_model=BudgetOut, status_code=status.HTTP_201_CREATED, summary="Budget anlegen" ) async def create_budget(payload: BudgetCreate, session: DbSession) -> Budget: await get_or_404(session, Category, payload.category_id) budget = Budget(**payload.model_dump()) session.add(budget) try: await session.commit() except IntegrityError as exc: await session.rollback() raise ConflictError( "Für diese Kategorie und diesen Monat gibt es bereits ein Budget." ) from exc await session.refresh(budget) return budget @router.patch( "/{budget_id}", response_model=BudgetOut, responses=NOT_FOUND, summary="Budget ändern" ) async def update_budget(budget_id: int, payload: BudgetUpdate, session: DbSession) -> Budget: budget = await get_or_404(session, Budget, budget_id) apply_updates(budget, payload) await session.commit() await session.refresh(budget) return budget @router.delete( "/{budget_id}", response_model=MessageResponse, responses=NOT_FOUND, summary="Budget löschen", ) async def delete_budget(budget_id: int, session: DbSession) -> MessageResponse: budget = await get_or_404(session, Budget, budget_id) await session.delete(budget) await session.commit() return MessageResponse(detail="Budget gelöscht.") # --- Vorlagen ------------------------------------------------------------------ templates = APIRouter(prefix="/budget-templates", tags=["budgets"]) @templates.get( "", response_model=list[BudgetTemplateOut], summary="Budgetvorlagen auflisten", description="Vorlagen gelten ab `valid_from` dauerhaft, sodass nicht jeder " "Monat einzeln gepflegt werden muss.", ) async def list_templates( session: DbSession, category_id: int | None = Query(default=None) ) -> list[BudgetTemplate]: stmt = select(BudgetTemplate).order_by( BudgetTemplate.category_id, BudgetTemplate.valid_from.desc() ) if category_id is not None: stmt = stmt.where(BudgetTemplate.category_id == category_id) return list((await session.execute(stmt)).scalars().all()) @templates.post( "", response_model=BudgetTemplateOut, status_code=status.HTTP_201_CREATED, summary="Budgetvorlage anlegen", ) async def create_template(payload: BudgetTemplateCreate, session: DbSession) -> BudgetTemplate: await get_or_404(session, Category, payload.category_id) if payload.valid_until is not None and payload.valid_until < payload.valid_from: raise ValidationError( "'valid_until' darf nicht vor 'valid_from' liegen.", code="invalid_date_range" ) template = BudgetTemplate(**payload.model_dump()) session.add(template) try: await session.commit() except IntegrityError as exc: await session.rollback() raise ConflictError( "Für diese Kategorie gibt es ab diesem Monat bereits eine Vorlage." ) from exc await session.refresh(template) return template @templates.patch( "/{template_id}", response_model=BudgetTemplateOut, responses=NOT_FOUND, summary="Budgetvorlage ändern", ) async def update_template( template_id: int, payload: BudgetTemplateUpdate, session: DbSession ) -> BudgetTemplate: template = await get_or_404(session, BudgetTemplate, template_id) if payload.valid_until is not None and payload.valid_until < template.valid_from: raise ValidationError( "'valid_until' darf nicht vor 'valid_from' liegen.", code="invalid_date_range" ) apply_updates(template, payload) await session.commit() await session.refresh(template) return template @templates.delete( "/{template_id}", response_model=MessageResponse, responses=NOT_FOUND, summary="Budgetvorlage löschen", ) async def delete_template(template_id: int, session: DbSession) -> MessageResponse: template = await get_or_404(session, BudgetTemplate, template_id) await session.delete(template) await session.commit() return MessageResponse(detail="Budgetvorlage gelöscht.")