Complete CarPass product flows
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from pydantic import BaseModel, ConfigDict, field_validator
|
||||
|
||||
from app.services.vehicle_identity import validate_vin
|
||||
|
||||
|
||||
class CarBase(BaseModel):
|
||||
@@ -9,10 +11,15 @@ class CarBase(BaseModel):
|
||||
make: str | None = None
|
||||
model: str | None = None
|
||||
trim: str | None = None
|
||||
generation: str | None = None
|
||||
body_type: str | None = None
|
||||
year: int | None = None
|
||||
plate_number: str | None = None
|
||||
vin: str | None = None
|
||||
fuel_type: str | None = None
|
||||
engine_volume_l: Decimal | None = None
|
||||
transmission: str | None = None
|
||||
drive_type: str | None = None
|
||||
target_consumption_l_per_100km: Decimal | None = None
|
||||
fuel_tank_volume_l: Decimal | None = None
|
||||
engine_oil_type: str | None = None
|
||||
@@ -23,11 +30,33 @@ class CarBase(BaseModel):
|
||||
brake_fluid_type: str | None = None
|
||||
tire_pressure_front_bar: Decimal | None = None
|
||||
tire_pressure_rear_bar: Decimal | None = None
|
||||
tire_size: str | None = None
|
||||
oil_change_interval_km: int | None = None
|
||||
oil_change_interval_months: int | None = None
|
||||
purchase_date: date | None = None
|
||||
purchase_price: Decimal | None = None
|
||||
purchase_currency: str | None = None
|
||||
purchase_type: str = "unknown"
|
||||
currency: str = "RUB"
|
||||
include_depreciation: bool = False
|
||||
expected_ownership_months: int | None = None
|
||||
expected_residual_value: Decimal | None = None
|
||||
loan_principal: Decimal | None = None
|
||||
loan_down_payment: Decimal | None = None
|
||||
loan_term_months: int | None = None
|
||||
loan_annual_interest_rate: Decimal | None = None
|
||||
loan_first_payment_date: date | None = None
|
||||
loan_payment_day: int | None = None
|
||||
loan_payment_type: str = "annuity"
|
||||
loan_currency: str | None = None
|
||||
loan_comment: str | None = None
|
||||
current_odometer: int | None = None
|
||||
notes: str | None = None
|
||||
|
||||
@field_validator("vin")
|
||||
@classmethod
|
||||
def validate_vin_field(cls, value: str | None) -> str | None:
|
||||
return validate_vin(value)
|
||||
|
||||
|
||||
class CarCreate(CarBase):
|
||||
@@ -39,10 +68,15 @@ class CarUpdate(BaseModel):
|
||||
make: str | None = None
|
||||
model: str | None = None
|
||||
trim: str | None = None
|
||||
generation: str | None = None
|
||||
body_type: str | None = None
|
||||
year: int | None = None
|
||||
plate_number: str | None = None
|
||||
vin: str | None = None
|
||||
fuel_type: str | None = None
|
||||
engine_volume_l: Decimal | None = None
|
||||
transmission: str | None = None
|
||||
drive_type: str | None = None
|
||||
target_consumption_l_per_100km: Decimal | None = None
|
||||
fuel_tank_volume_l: Decimal | None = None
|
||||
engine_oil_type: str | None = None
|
||||
@@ -53,11 +87,33 @@ class CarUpdate(BaseModel):
|
||||
brake_fluid_type: str | None = None
|
||||
tire_pressure_front_bar: Decimal | None = None
|
||||
tire_pressure_rear_bar: Decimal | None = None
|
||||
tire_size: str | None = None
|
||||
oil_change_interval_km: int | None = None
|
||||
oil_change_interval_months: int | None = None
|
||||
purchase_date: date | None = None
|
||||
purchase_price: Decimal | None = None
|
||||
purchase_currency: str | None = None
|
||||
purchase_type: str | None = None
|
||||
currency: str | None = None
|
||||
include_depreciation: bool | None = None
|
||||
expected_ownership_months: int | None = None
|
||||
expected_residual_value: Decimal | None = None
|
||||
loan_principal: Decimal | None = None
|
||||
loan_down_payment: Decimal | None = None
|
||||
loan_term_months: int | None = None
|
||||
loan_annual_interest_rate: Decimal | None = None
|
||||
loan_first_payment_date: date | None = None
|
||||
loan_payment_day: int | None = None
|
||||
loan_payment_type: str | None = None
|
||||
loan_currency: str | None = None
|
||||
loan_comment: str | None = None
|
||||
current_odometer: int | None = None
|
||||
notes: str | None = None
|
||||
|
||||
@field_validator("vin")
|
||||
@classmethod
|
||||
def validate_vin_field(cls, value: str | None) -> str | None:
|
||||
return validate_vin(value)
|
||||
|
||||
|
||||
class CarRead(CarBase):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
|
||||
from app.models.expense import ExpenseCategory, ServiceType
|
||||
|
||||
@@ -14,18 +14,27 @@ class FuelEntryBase(BaseModel):
|
||||
total_cost: Decimal | None = None
|
||||
station: str | None = None
|
||||
fuel_brand: str | None = None
|
||||
is_full_tank: bool = True
|
||||
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):
|
||||
@@ -38,6 +47,7 @@ class FuelEntryUpdate(BaseModel):
|
||||
fuel_brand: str | None = None
|
||||
is_full_tank: bool | None = None
|
||||
notes: str | None = None
|
||||
confirm_lower_odometer: bool = False
|
||||
|
||||
|
||||
class FuelEntryRead(FuelEntryBase):
|
||||
@@ -61,9 +71,20 @@ class ServiceEntryBase(BaseModel):
|
||||
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):
|
||||
@@ -77,6 +98,7 @@ class ServiceEntryUpdate(BaseModel):
|
||||
next_due_date: date | None = None
|
||||
next_due_odometer: int | None = None
|
||||
notes: str | None = None
|
||||
confirm_lower_odometer: bool = False
|
||||
|
||||
|
||||
class ServiceEntryRead(ServiceEntryBase):
|
||||
@@ -99,19 +121,36 @@ class ExpenseEntryBase(BaseModel):
|
||||
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):
|
||||
@@ -126,7 +165,20 @@ class ExpenseEntryUpdate(BaseModel):
|
||||
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):
|
||||
@@ -151,11 +203,23 @@ class OwnershipStats(BaseModel):
|
||||
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
|
||||
@@ -179,5 +243,25 @@ class OdometerPrediction(BaseModel):
|
||||
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)
|
||||
|
||||
@@ -114,20 +114,49 @@ class VehicleCreate(BaseModel):
|
||||
name: str
|
||||
make: str | None = None
|
||||
model: str | None = None
|
||||
trim: str | None = None
|
||||
generation: str | None = None
|
||||
body_type: str | None = None
|
||||
year: int | None = None
|
||||
license_plate: str | None = None
|
||||
license_plate_country: str | None = None
|
||||
vin: str | None = None
|
||||
current_odometer: int | None = None
|
||||
fuel_type: str | None = None
|
||||
engine_volume_l: Decimal | None = None
|
||||
transmission: str | None = None
|
||||
drive_type: str | None = None
|
||||
engine_oil_type: str | None = None
|
||||
engine_oil_volume_l: Decimal | None = None
|
||||
transmission_fluid_type: str | None = None
|
||||
transmission_fluid_volume_l: Decimal | None = None
|
||||
coolant_type: str | None = None
|
||||
brake_fluid_type: str | None = None
|
||||
tire_pressure_front_bar: Decimal | None = None
|
||||
tire_pressure_rear_bar: Decimal | None = None
|
||||
tire_size: str | None = None
|
||||
oil_change_interval_km: int | None = None
|
||||
oil_change_interval_months: int | None = None
|
||||
fuel_tank_volume_l: Decimal | None = None
|
||||
target_consumption_l_per_100km: Decimal | None = None
|
||||
purchase_date: date | None = None
|
||||
purchase_price: Decimal | None = None
|
||||
purchase_currency: str | None = None
|
||||
purchase_type: str = "unknown"
|
||||
currency: str = "RUB"
|
||||
include_depreciation: bool = False
|
||||
expected_ownership_months: int | None = None
|
||||
expected_residual_value: Decimal | None = None
|
||||
loan_principal: Decimal | None = None
|
||||
loan_down_payment: Decimal | None = None
|
||||
loan_term_months: int | None = None
|
||||
loan_annual_interest_rate: Decimal | None = None
|
||||
loan_first_payment_date: date | None = None
|
||||
loan_payment_day: int | None = None
|
||||
loan_payment_type: str = "annuity"
|
||||
loan_currency: str | None = None
|
||||
loan_comment: str | None = None
|
||||
notes: str | None = None
|
||||
|
||||
@field_validator("vin")
|
||||
@classmethod
|
||||
@@ -139,20 +168,49 @@ class VehicleUpdate(BaseModel):
|
||||
name: str | None = None
|
||||
make: str | None = None
|
||||
model: str | None = None
|
||||
trim: str | None = None
|
||||
generation: str | None = None
|
||||
body_type: str | None = None
|
||||
year: int | None = None
|
||||
license_plate: str | None = None
|
||||
license_plate_country: str | None = None
|
||||
vin: str | None = None
|
||||
current_odometer: int | None = None
|
||||
fuel_type: str | None = None
|
||||
engine_volume_l: Decimal | None = None
|
||||
transmission: str | None = None
|
||||
drive_type: str | None = None
|
||||
fuel_tank_volume_l: Decimal | None = None
|
||||
target_consumption_l_per_100km: Decimal | None = None
|
||||
purchase_date: date | None = None
|
||||
purchase_price: Decimal | None = None
|
||||
purchase_currency: str | None = None
|
||||
purchase_type: str | None = None
|
||||
currency: str | None = None
|
||||
include_depreciation: bool | None = None
|
||||
expected_ownership_months: int | None = None
|
||||
expected_residual_value: Decimal | None = None
|
||||
engine_oil_type: str | None = None
|
||||
engine_oil_volume_l: Decimal | None = None
|
||||
transmission_fluid_type: str | None = None
|
||||
transmission_fluid_volume_l: Decimal | None = None
|
||||
coolant_type: str | None = None
|
||||
brake_fluid_type: str | None = None
|
||||
tire_pressure_front_bar: Decimal | None = None
|
||||
tire_pressure_rear_bar: Decimal | None = None
|
||||
tire_size: str | None = None
|
||||
oil_change_interval_km: int | None = None
|
||||
oil_change_interval_months: int | None = None
|
||||
loan_principal: Decimal | None = None
|
||||
loan_down_payment: Decimal | None = None
|
||||
loan_term_months: int | None = None
|
||||
loan_annual_interest_rate: Decimal | None = None
|
||||
loan_first_payment_date: date | None = None
|
||||
loan_payment_day: int | None = None
|
||||
loan_payment_type: str | None = None
|
||||
loan_currency: str | None = None
|
||||
loan_comment: str | None = None
|
||||
notes: str | None = None
|
||||
|
||||
@field_validator("vin")
|
||||
@classmethod
|
||||
@@ -166,20 +224,49 @@ class VehicleRead(BaseModel):
|
||||
name: str
|
||||
make: str | None = None
|
||||
model: str | None = None
|
||||
trim: str | None = None
|
||||
generation: str | None = None
|
||||
body_type: str | None = None
|
||||
year: int | None = None
|
||||
license_plate_display: str | None = None
|
||||
license_plate_country: str | None = None
|
||||
vin_normalized: str | None = None
|
||||
current_odometer: int | None = None
|
||||
fuel_type: str | None = None
|
||||
engine_volume_l: Decimal | None = None
|
||||
transmission: str | None = None
|
||||
drive_type: str | None = None
|
||||
fuel_tank_volume_l: Decimal | None = None
|
||||
target_consumption_l_per_100km: Decimal | None = None
|
||||
purchase_date: date | None = None
|
||||
purchase_price: Decimal | None = None
|
||||
purchase_currency: str | None = None
|
||||
purchase_type: str = "unknown"
|
||||
currency: str = "RUB"
|
||||
include_depreciation: bool = False
|
||||
expected_ownership_months: int | None = None
|
||||
expected_residual_value: Decimal | None = None
|
||||
engine_oil_type: str | None = None
|
||||
engine_oil_volume_l: Decimal | None = None
|
||||
transmission_fluid_type: str | None = None
|
||||
transmission_fluid_volume_l: Decimal | None = None
|
||||
coolant_type: str | None = None
|
||||
brake_fluid_type: str | None = None
|
||||
tire_pressure_front_bar: Decimal | None = None
|
||||
tire_pressure_rear_bar: Decimal | None = None
|
||||
tire_size: str | None = None
|
||||
oil_change_interval_km: int | None = None
|
||||
oil_change_interval_months: int | None = None
|
||||
loan_principal: Decimal | None = None
|
||||
loan_down_payment: Decimal | None = None
|
||||
loan_term_months: int | None = None
|
||||
loan_annual_interest_rate: Decimal | None = None
|
||||
loan_first_payment_date: date | None = None
|
||||
loan_payment_day: int | None = None
|
||||
loan_payment_type: str = "annuity"
|
||||
loan_currency: str | None = None
|
||||
loan_comment: str | None = None
|
||||
notes: str | None = None
|
||||
created_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -358,4 +445,9 @@ class ServiceInboxRead(ServiceInboxCreate):
|
||||
error: str | None = None
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class AdminModerationDecision(BaseModel):
|
||||
comment: str | None = None
|
||||
reason: str | None = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
Reference in New Issue
Block a user