from datetime import date, datetime from decimal import Decimal from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from app.models.expense import ExpenseCategory, ServiceType class FuelEntryBase(BaseModel): entry_date: date odometer: int liters: Decimal price_per_liter: Decimal total_cost: Decimal | None = None station: str | None = None fuel_brand: str | None = None is_full_tank: bool | None = None notes: str | None = None @model_validator(mode="after") def fill_total_cost(self) -> "FuelEntryBase": if self.odometer < 0: raise ValueError("odometer must be non-negative") if self.liters <= 0: raise ValueError("liters must be positive") if self.price_per_liter <= 0: raise ValueError("price_per_liter must be positive") if self.total_cost is None: self.total_cost = self.liters * self.price_per_liter if self.total_cost <= 0: raise ValueError("total_cost must be positive") return self class FuelEntryCreate(FuelEntryBase): car_id: int confirm_lower_odometer: bool = False class FuelEntryUpdate(BaseModel): entry_date: date | None = None odometer: int | None = None liters: Decimal | None = None price_per_liter: Decimal | None = None total_cost: Decimal | None = None station: str | None = None fuel_brand: str | None = None is_full_tank: bool | None = None notes: str | None = None confirm_lower_odometer: bool = False class FuelEntryRead(FuelEntryBase): id: int car_id: int total_cost: Decimal created_at: datetime model_config = ConfigDict(from_attributes=True) class ServiceEntryBase(BaseModel): entry_date: date odometer: int | None = None service_type: ServiceType title: str category: str | None = None vendor: str | None = None total_cost: Decimal next_due_date: date | None = None next_due_odometer: int | None = None notes: str | None = None @model_validator(mode="after") def validate_service(self) -> "ServiceEntryBase": if self.odometer is not None and self.odometer < 0: raise ValueError("odometer must be non-negative") if self.total_cost < 0: raise ValueError("total_cost must be non-negative") if not self.title.strip(): raise ValueError("title is required") return self class ServiceEntryCreate(ServiceEntryBase): car_id: int confirm_lower_odometer: bool = False class ServiceEntryUpdate(BaseModel): entry_date: date | None = None odometer: int | None = None service_type: ServiceType | None = None title: str | None = None category: str | None = None vendor: str | None = None total_cost: Decimal | None = None next_due_date: date | None = None next_due_odometer: int | None = None notes: str | None = None confirm_lower_odometer: bool = False class ServiceEntryRead(ServiceEntryBase): id: int car_id: int created_at: datetime model_config = ConfigDict(from_attributes=True) class ExpenseEntryBase(BaseModel): entry_date: date category: ExpenseCategory title: str vendor: str | None = None total_cost: Decimal currency: str = "RUB" odometer: int | None = None period_start: date | None = None period_end: date | None = None period_months: int | None = None is_recurring: bool = False policy_number: str | None = None insurance_type: str | None = None payment_period_months: int | None = None document_urls: list[str] | None = None metadata_json: dict | None = None notes: str | None = None @model_validator(mode="after") def validate_period(self) -> "ExpenseEntryBase": if self.total_cost <= 0: raise ValueError("total_cost must be positive") if self.odometer is not None and self.odometer < 0: raise ValueError("odometer must be non-negative") if self.period_months is not None and self.period_months < 1: raise ValueError("period_months must be positive") if self.payment_period_months is not None and self.payment_period_months < 1: raise ValueError("payment_period_months must be positive") if self.period_start and self.period_end and self.period_end < self.period_start: raise ValueError("period_end must be after period_start") if self.category == ExpenseCategory.insurance: if self.period_start and self.period_end: return self if self.period_months or self.payment_period_months: return self return self class ExpenseEntryCreate(ExpenseEntryBase): car_id: int confirm_lower_odometer: bool = False class ExpenseEntryUpdate(BaseModel): entry_date: date | None = None category: ExpenseCategory | None = None title: str | None = None vendor: str | None = None total_cost: Decimal | None = None currency: str | None = None odometer: int | None = None period_start: date | None = None period_end: date | None = None period_months: int | None = None is_recurring: bool | None = None policy_number: str | None = None insurance_type: str | None = None payment_period_months: int | None = None document_urls: list[str] | None = None metadata_json: dict | None = None notes: str | None = None confirm_lower_odometer: bool = False @field_validator("total_cost") @classmethod def validate_total_cost(cls, value: Decimal | None) -> Decimal | None: if value is not None and value <= 0: raise ValueError("total_cost must be positive") return value class ExpenseEntryRead(ExpenseEntryBase): id: int car_id: int created_at: datetime model_config = ConfigDict(from_attributes=True) class OwnershipCategoryBreakdown(BaseModel): category: str total_cost: Decimal entries_count: int class OwnershipStats(BaseModel): car_id: int date_from: date date_to: date fuel_cost: Decimal service_cost: Decimal total_cost: Decimal expenses_cost: Decimal = Decimal("0") repair_cost: Decimal = Decimal("0") fixed_costs: Decimal = Decimal("0") variable_costs: Decimal = Decimal("0") recurring_costs: Decimal = Decimal("0") one_time_costs: Decimal = Decimal("0") forecast_next_month: Decimal = Decimal("0") depreciation_cost: Decimal = Decimal("0") loan_principal_cost: Decimal = Decimal("0") loan_interest_cost: Decimal = Decimal("0") total_cost_without_credit: Decimal = Decimal("0") total_cost_with_credit: Decimal = Decimal("0") cost_per_day: Decimal = Decimal("0") cost_per_month: Decimal = Decimal("0") current_month_cost: Decimal = Decimal("0") previous_month_cost: Decimal = Decimal("0") month_over_month_change_pct: float | None = None cost_warning: str | None = None cost_by_category: dict[str, Decimal] = Field(default_factory=dict) categories: list[OwnershipCategoryBreakdown] = Field(default_factory=list) liters: Decimal distance_km: int avg_consumption_l_per_100km: float | None cost_per_km: float | None fuel_entries_count: int service_entries_count: int class OdometerPrediction(BaseModel): car_id: int samples: int current_odometer: int | None predicted_today: int | None predicted_30_days: int | None avg_km_per_day: float | None avg_km_per_month: float | None current_price_per_liter: float | None = None predicted_price_per_liter_30_days: float | None = None avg_price_per_liter: float | None = None price_samples: int = 0 price_confidence: float = 0 average_full_tank_distance: float | None = None average_fuel_consumption_full_tank: float | None = None average_cost_per_full_tank: float | None = None last_full_tank_distance: int | None = None full_tank_warning: str | None = None confidence: float insight: str class OdometerHistoryRead(BaseModel): id: int car_id: int previous_odometer: int | None = None new_odometer: int source_record_type: str source_record_id: int | None = None changed_at: datetime changed_by: int | None = None confirmation_required: bool user_confirmed: bool model_config = ConfigDict(from_attributes=True)