Improve CarPass product UX and service flows
This commit is contained in:
@@ -25,6 +25,8 @@ class CarBase(BaseModel):
|
||||
tire_pressure_rear_bar: Decimal | None = None
|
||||
purchase_date: date | None = None
|
||||
purchase_price: Decimal | None = None
|
||||
currency: str = "RUB"
|
||||
include_depreciation: bool = False
|
||||
current_odometer: int | None = None
|
||||
|
||||
|
||||
@@ -53,6 +55,8 @@ class CarUpdate(BaseModel):
|
||||
tire_pressure_rear_bar: Decimal | None = None
|
||||
purchase_date: date | None = None
|
||||
purchase_price: Decimal | None = None
|
||||
currency: str | None = None
|
||||
include_depreciation: bool | None = None
|
||||
current_odometer: int | None = None
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, model_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
|
||||
from app.models.expense import ServiceType
|
||||
from app.models.expense import ExpenseCategory, ServiceType
|
||||
|
||||
|
||||
class FuelEntryBase(BaseModel):
|
||||
@@ -87,6 +87,62 @@ class ServiceEntryRead(ServiceEntryBase):
|
||||
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
|
||||
notes: str | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_period(self) -> "ExpenseEntryBase":
|
||||
if self.period_months is not None and self.period_months < 1:
|
||||
raise ValueError("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")
|
||||
return self
|
||||
|
||||
|
||||
class ExpenseEntryCreate(ExpenseEntryBase):
|
||||
car_id: int
|
||||
|
||||
|
||||
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
|
||||
notes: str | None = None
|
||||
|
||||
|
||||
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
|
||||
@@ -94,6 +150,14 @@ class OwnershipStats(BaseModel):
|
||||
fuel_cost: Decimal
|
||||
service_cost: Decimal
|
||||
total_cost: Decimal
|
||||
expenses_cost: 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")
|
||||
cost_per_month: Decimal = Decimal("0")
|
||||
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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, field_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
|
||||
from app.services.vehicle_identity import normalize_license_plate, validate_vin
|
||||
|
||||
@@ -16,6 +16,13 @@ class ServiceCenterCreate(BaseModel):
|
||||
business_registration_number: str | None = None
|
||||
telegram_chat_id: str | None = None
|
||||
contact_phone: str | None = None
|
||||
description: str | None = None
|
||||
specializations: list[str] | None = None
|
||||
working_hours: str | None = None
|
||||
facade_photo_url: str | None = None
|
||||
document_photo_urls: list[str] | None = None
|
||||
additional_photo_urls: list[str] | None = None
|
||||
contact_person: str | None = None
|
||||
|
||||
|
||||
class ServiceCenterRead(ServiceCenterCreate):
|
||||
@@ -26,6 +33,27 @@ class ServiceCenterRead(ServiceCenterCreate):
|
||||
created_at: datetime
|
||||
verified_at: datetime | None = None
|
||||
suspended_at: datetime | None = None
|
||||
rating_avg: Decimal | None = None
|
||||
reviews_count: int = 0
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class ServiceCenterPublicRead(BaseModel):
|
||||
id: int
|
||||
display_name: str | None = None
|
||||
name: str
|
||||
country: str | None = None
|
||||
city: str | None = None
|
||||
address: str | None = None
|
||||
phone: str | None = None
|
||||
description: str | None = None
|
||||
specializations: list[str] | None = None
|
||||
working_hours: str | None = None
|
||||
facade_photo_url: str | None = None
|
||||
verification_status: str
|
||||
rating_avg: Decimal | None = None
|
||||
reviews_count: int = 0
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@@ -91,8 +119,15 @@ class VehicleCreate(BaseModel):
|
||||
license_plate_country: str | None = None
|
||||
vin: str | None = None
|
||||
current_odometer: int | None = None
|
||||
fuel_type: str | None = None
|
||||
engine_oil_type: str | None = None
|
||||
engine_oil_volume_l: Decimal | 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
|
||||
currency: str = "RUB"
|
||||
include_depreciation: bool = False
|
||||
|
||||
@field_validator("vin")
|
||||
@classmethod
|
||||
@@ -109,6 +144,13 @@ class VehicleUpdate(BaseModel):
|
||||
license_plate_country: str | None = None
|
||||
vin: str | None = None
|
||||
current_odometer: int | None = None
|
||||
fuel_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
|
||||
currency: str | None = None
|
||||
include_depreciation: bool | None = None
|
||||
engine_oil_type: str | None = None
|
||||
engine_oil_volume_l: Decimal | None = None
|
||||
|
||||
@@ -129,6 +171,13 @@ class VehicleRead(BaseModel):
|
||||
license_plate_country: str | None = None
|
||||
vin_normalized: str | None = None
|
||||
current_odometer: int | None = None
|
||||
fuel_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
|
||||
currency: str = "RUB"
|
||||
include_depreciation: bool = False
|
||||
engine_oil_type: str | None = None
|
||||
engine_oil_volume_l: Decimal | None = None
|
||||
created_at: datetime
|
||||
@@ -226,11 +275,70 @@ class CarServiceLinkCreate(BaseModel):
|
||||
car_id: int
|
||||
service_center_id: int
|
||||
external_vehicle_ref: str | None = None
|
||||
access_level: str = "basic"
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
class CarServiceLinkRead(CarServiceLinkCreate):
|
||||
id: int
|
||||
status: str = "pending"
|
||||
requested_by_user_id: int | None = None
|
||||
approved_by_user_id: int | None = None
|
||||
approved_at: datetime | None = None
|
||||
revoked_at: datetime | None = None
|
||||
created_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class ServiceCenterAccessRequest(BaseModel):
|
||||
car_id: int
|
||||
access_level: str = "basic"
|
||||
external_vehicle_ref: str | None = None
|
||||
|
||||
@field_validator("access_level")
|
||||
@classmethod
|
||||
def validate_access_level(cls, value: str) -> str:
|
||||
allowed = {"basic", "service_history", "full"}
|
||||
if value not in allowed:
|
||||
raise ValueError(f"access_level must be one of {', '.join(sorted(allowed))}")
|
||||
return value
|
||||
|
||||
|
||||
class ServiceCenterReviewCreate(BaseModel):
|
||||
rating: int = Field(ge=1, le=5)
|
||||
text: str | None = None
|
||||
photo_urls: list[str] | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_review(self) -> "ServiceCenterReviewCreate":
|
||||
if self.text is not None and len(self.text.strip()) < 3:
|
||||
raise ValueError("review text is too short")
|
||||
return self
|
||||
|
||||
|
||||
class ServiceCenterReviewRead(ServiceCenterReviewCreate):
|
||||
id: int
|
||||
service_center_id: int
|
||||
user_id: int
|
||||
status: str
|
||||
service_response: str | None = None
|
||||
service_responded_at: datetime | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class ServiceCenterReviewCommentCreate(BaseModel):
|
||||
text: str = Field(min_length=2, max_length=2000)
|
||||
|
||||
|
||||
class ServiceCenterReviewCommentRead(ServiceCenterReviewCommentCreate):
|
||||
id: int
|
||||
review_id: int
|
||||
user_id: int
|
||||
status: str
|
||||
created_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
Reference in New Issue
Block a user