"""Schemata der Auswertungen.""" from datetime import date from pydantic import Field from app.models.enums import EntryKind, OccurrenceStatus from app.schemas.common import ApiModel, Money from app.schemas.recurrence import ContractTermOut class TotalsOut(ApiModel): """Einnahmen, Ausgaben und Saldo einer Sicht.""" income: Money expenses: Money balance: Money class MonthComparisonOut(ApiModel): """Veränderung gegenüber dem Vormonat.""" income: Money expenses: Money balance: Money class MonthReportOut(ApiModel): """Monatsübersicht mit Plan-Ist-Vergleich.""" month: date = Field(description="Immer der Monatserste.") planned: TotalsOut = Field(description="Soll aus Fälligkeiten und Buchungen.") actual: TotalsOut = Field( description="Ist aus bestätigten Fälligkeiten und allen einmaligen Buchungen." ) previous_planned: TotalsOut previous_actual: TotalsOut delta_to_previous: MonthComparisonOut fixed_costs: Money = Field(description="Ausgaben in Kategorien mit `is_fixed_cost`.") variable_costs: Money reserves: Money = Field(description="Summe der monatlichen Rücklagen.") available_after_fixed: Money = Field(description="Einkünfte abzüglich Fixkosten und Rücklagen.") confirmed_count: int open_count: int skipped_count: int class ForecastMonthOut(ApiModel): """Ein Monat der Vorschau.""" month: date income: Money expenses: Money balance: Money cumulative_balance: Money = Field( description="Prognostizierter Kontostand am Monatsende über alle Konten." ) class ForecastOut(ApiModel): months: list[ForecastMonthOut] total_income: Money total_expenses: Money class CategorySliceOut(ApiModel): """Summe einer Kategorie; Oberkategorien tragen ihre Unterkategorien.""" category_id: int name: str color: str icon: str amount: Money count: int children: list["CategorySliceOut"] = Field(default_factory=list) class CategoryReportOut(ApiModel): date_from: date date_to: date kind: EntryKind total: Money categories: list[CategorySliceOut] class SubscriptionOut(ApiModel): """Ein laufender Posten mit Jahreskosten.""" recurrence_id: int title: str merchant_id: int | None merchant_name: str | None category_id: int amount: Money annual_cost: Money monthly_cost: Money rrule: str is_installment: bool = Field( description="Ratenzahlungen zählen nicht in die Summe „Abos gesamt p. a.“." ) is_cancelled: bool contract_term: ContractTermOut | None = None days_until_notice: int | None = Field( default=None, description="Tage bis zum letzten Kündigungstermin." ) class SubscriptionReportOut(ApiModel): entries: list[SubscriptionOut] total_annual: Money = Field(description="Summe über alle Posten ohne Ratenzahlungen.") total_monthly: Money upcoming_deadlines: list[SubscriptionOut] = Field( description="Kündigungsfristen, die in den nächsten 60 Tagen ablaufen." ) class YearComparisonRowOut(ApiModel): category_id: int name: str color: str current: Money previous: Money delta: Money class YearComparisonOut(ApiModel): year: int rows: list[YearComparisonRowOut] current_total: Money previous_total: Money class CalendarEntryOut(ApiModel): """Ein fälliger Posten im Kalender.""" title: str kind: EntryKind amount: Money category_id: int merchant_id: int | None account_id: int | None source: str = Field(description="'recurrence' oder 'transaction'.") recurrence_id: int | None = None occurrence_date: date | None = Field( default=None, description="Nominales Datum – Schlüssel für Bestätigen und Auslassen." ) status: OccurrenceStatus | None = None is_variable: bool = False class CalendarDayOut(ApiModel): date: date entries: list[CalendarEntryOut] net: Money = Field(description="Saldo des Tages, Ausgaben negativ.") running_balance: Money is_business_day: bool class CalendarMonthOut(ApiModel): month: date days: list[CalendarDayOut] opening_balance: Money closing_balance: Money lowest_balance: Money lowest_balance_on: date | None class BudgetStatusOut(ApiModel): """Stand eines Budgets samt Ampel.""" category_id: int category_name: str color: str period_month: date limit_amount: Money carried_over: Money = Field(description="Übertrag aus Vormonaten bei aktivem Rollover.") available: Money = Field(description="Limit zuzüglich Übertrag.") spent: Money remaining: Money ratio: float = Field(description="Verbrauchsanteil; 1.0 entspricht 100 %.") state: str = Field(description="'ok' unter 80 %, 'warning' unter 100 %, sonst 'exceeded'.") rollover: bool is_template: bool class SavingsGoalProgressOut(ApiModel): """Fortschritt eines Sparziels.""" goal_id: int name: str color: str icon: str target_amount: Money current_amount: Money remaining_amount: Money ratio: float target_date: date | None months_left: int | None required_monthly: Money | None = Field( default=None, description="Nötige Rate bis zum Zieldatum." ) monthly_contribution: Money | None is_on_track: bool | None = Field( default=None, description="Reicht die geplante Rate bis zum Zieldatum?" ) class DashboardOut(ApiModel): """Alles, was das Dashboard in einem Aufruf braucht.""" month: MonthReportOut total_balance: Money forecast: list[ForecastMonthOut] categories: list[CategorySliceOut] budgets: list[BudgetStatusOut] goals: list[SavingsGoalProgressOut] upcoming: list[CalendarEntryOut] = Field( description="Die nächsten Fälligkeiten der kommenden 14 Tage." ) upcoming_deadlines: list[SubscriptionOut]