"""Fälligkeiten über alle Posten hinweg: abrufen, bestätigen, auslassen.""" from datetime import date from fastapi import APIRouter, Query, status from app.api.deps import DbSession from app.api.routes.recurrences import to_occurrence_out from app.core.clock import add_months, today from app.core.errors import ValidationError from app.models.enums import EntryKind, OccurrenceStatus from app.schemas.common import ErrorResponse, MessageResponse from app.schemas.occurrence import ( OccurrenceConfirm, OccurrenceOut, OccurrenceReset, OccurrenceSkip, ) from app.services import occurrences as service router = APIRouter(prefix="/occurrences", tags=["occurrences"]) NOT_FOUND = {status.HTTP_404_NOT_FOUND: {"model": ErrorResponse}} @router.get( "", response_model=list[OccurrenceOut], summary="Fälligkeiten im Zeitraum", description="Expandiert alle passenden Posten und überlagert sie mit den " "erfassten Abweichungen. Ohne Angabe umfasst das Fenster die nächsten drei Monate.", ) async def list_occurrences( session: DbSession, date_from: date | None = Query(default=None, alias="from"), date_to: date | None = Query(default=None, alias="to"), kind: EntryKind | None = Query(default=None), category_id: int | None = Query(default=None), account_id: int | None = Query(default=None), recurrence_id: int | None = Query(default=None), status_filter: OccurrenceStatus | None = Query( default=None, alias="status", description="Nach Status filtern." ), include_inactive: bool = Query( default=False, description="Auch deaktivierte Posten einbeziehen." ), by_due_date: bool = Query( default=True, description="True gruppiert nach dem tatsächlichen Zahltag, False nach dem " "nominalen Datum.", ), ) -> list[OccurrenceOut]: start = date_from or today() end = date_to or add_months(start, 3) if end < start: raise ValidationError("'to' darf nicht vor 'from' liegen.", code="invalid_date_range") items = await service.due_items( session, start, end, by_due_date=by_due_date, kind=kind, only_active=not include_inactive, category_id=category_id, account_id=account_id, recurrence_id=recurrence_id, ) if status_filter is not None: items = [item for item in items if item.planned.status is status_filter] return [to_occurrence_out(item.planned, item.recurrence) for item in items] @router.post( "/confirm", response_model=OccurrenceOut, responses=NOT_FOUND, summary="Fälligkeit bestätigen", description="Schlüssel ist das nominale Datum. Ohne `actual_amount` gilt der Sollbetrag.", ) async def confirm_occurrence(payload: OccurrenceConfirm, session: DbSession) -> OccurrenceOut: await service.confirm( session, payload.recurrence_id, payload.occurrence_date, actual_amount=payload.actual_amount, actual_date=payload.actual_date, account_id=payload.account_id, note=payload.note, ) await session.commit() return await _single(session, payload.recurrence_id, payload.occurrence_date) @router.post( "/skip", response_model=OccurrenceOut, responses=NOT_FOUND, summary="Fälligkeit auslassen", description="Die Fälligkeit zählt danach in keiner Auswertung mehr mit.", ) async def skip_occurrence(payload: OccurrenceSkip, session: DbSession) -> OccurrenceOut: await service.skip(session, payload.recurrence_id, payload.occurrence_date, note=payload.note) await session.commit() return await _single(session, payload.recurrence_id, payload.occurrence_date) @router.post( "/reset", response_model=OccurrenceOut, responses=NOT_FOUND, summary="Bestätigung zurücknehmen", description="Löscht die erfasste Abweichung; die Fälligkeit gilt wieder als geplant.", ) async def reset_occurrence(payload: OccurrenceReset, session: DbSession) -> OccurrenceOut: await service.reset(session, payload.recurrence_id, payload.occurrence_date) await session.commit() return await _single(session, payload.recurrence_id, payload.occurrence_date) @router.delete( "/{recurrence_id}/{occurrence_date}", response_model=MessageResponse, responses=NOT_FOUND, summary="Erfasste Abweichung löschen", ) async def delete_occurrence( recurrence_id: int, occurrence_date: date, session: DbSession ) -> MessageResponse: await service.reset(session, recurrence_id, occurrence_date) await session.commit() return MessageResponse(detail="Erfasste Abweichung gelöscht.") async def _single(session: DbSession, recurrence_id: int, occurrence_date: date) -> OccurrenceOut: """Liest genau eine Fälligkeit nach einer Änderung frisch aus.""" recurrence = await service.get_recurrence(session, recurrence_id) items = await service.due_items( session, occurrence_date, occurrence_date, by_due_date=False, only_active=False, recurrence_id=recurrence_id, ) if not items: raise ValidationError( f"Zum {occurrence_date.isoformat()} gibt es für '{recurrence.title}' keine Fälligkeit.", code="occurrence_not_due", ) return to_occurrence_out(items[0].planned, items[0].recurrence)