Improve CarPass product UX and service flows

This commit is contained in:
VPN SaaS Dev
2026-05-14 19:33:25 +09:00
parent b85db333d8
commit caa5f6d3db
36 changed files with 1836 additions and 366 deletions

View File

@@ -3,6 +3,7 @@ from decimal import Decimal
from sqlalchemy import (
JSON,
Boolean,
Date,
DateTime,
ForeignKey,
@@ -47,6 +48,8 @@ class Car(Base):
tire_pressure_rear_bar: Mapped[Decimal | None] = mapped_column(Numeric(4, 2))
purchase_date: Mapped[date | None] = mapped_column(Date)
purchase_price: Mapped[Decimal | None] = mapped_column(Numeric(12, 2))
currency: Mapped[str] = mapped_column(String(3), default="RUB", server_default="RUB")
include_depreciation: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false")
current_odometer: Mapped[int | None]
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
@@ -56,6 +59,7 @@ class Car(Base):
owner = relationship("User", back_populates="cars")
fuel_entries = relationship("FuelEntry", back_populates="car", cascade="all, delete-orphan")
service_entries = relationship("ServiceEntry", back_populates="car", cascade="all, delete-orphan")
expense_entries = relationship("ExpenseEntry", back_populates="car", cascade="all, delete-orphan")
service_links = relationship("CarServiceLink", back_populates="car", cascade="all, delete-orphan")
@@ -115,6 +119,15 @@ class ServiceCenter(Base):
phone: Mapped[str | None] = mapped_column(String(40))
contact_phone: Mapped[str | None] = mapped_column(String(40))
address: Mapped[str | None] = mapped_column(String(240))
description: Mapped[str | None] = mapped_column(Text)
specializations: Mapped[list | None] = mapped_column(JSON)
working_hours: Mapped[str | None] = mapped_column(String(240))
facade_photo_url: Mapped[str | None] = mapped_column(String(500))
document_photo_urls: Mapped[list | None] = mapped_column(JSON)
additional_photo_urls: Mapped[list | None] = mapped_column(JSON)
contact_person: Mapped[str | None] = mapped_column(String(160))
rating_avg: Mapped[Decimal | None] = mapped_column(Numeric(3, 2))
reviews_count: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
business_registration_number: Mapped[str | None] = mapped_column(String(80))
verification_status: Mapped[str] = mapped_column(String(24), default="pending", server_default="pending", index=True)
owner_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), index=True)
@@ -126,6 +139,7 @@ class ServiceCenter(Base):
inbox_messages = relationship("ServiceInboxMessage", back_populates="service_center")
employees = relationship("ServiceEmployee", back_populates="service_center", cascade="all, delete-orphan")
visits = relationship("ServiceVisit", back_populates="service_center")
reviews = relationship("ServiceCenterReview", back_populates="service_center", cascade="all, delete-orphan")
class CarServiceLink(Base):
@@ -136,6 +150,12 @@ class CarServiceLink(Base):
car_id: Mapped[int] = mapped_column(ForeignKey("cars.id", ondelete="CASCADE"), index=True)
service_center_id: Mapped[int] = mapped_column(ForeignKey("service_centers.id", ondelete="CASCADE"), index=True)
external_vehicle_ref: Mapped[str | None] = mapped_column(String(120), index=True)
access_level: Mapped[str] = mapped_column(String(32), default="basic", server_default="basic", index=True)
status: Mapped[str] = mapped_column(String(32), default="pending", server_default="pending", index=True)
requested_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), index=True)
approved_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), index=True)
approved_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
is_active: Mapped[bool] = mapped_column(default=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
@@ -243,6 +263,41 @@ class ServiceWorkItem(Base):
visit = relationship("ServiceVisit", back_populates="work_items")
class ServiceCenterReview(Base):
__tablename__ = "service_center_reviews"
__table_args__ = (UniqueConstraint("service_center_id", "user_id", name="uq_service_review_user"),)
id: Mapped[int] = mapped_column(primary_key=True)
service_center_id: Mapped[int] = mapped_column(ForeignKey("service_centers.id", ondelete="CASCADE"), index=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True)
rating: Mapped[int] = mapped_column(Integer)
text: Mapped[str | None] = mapped_column(Text)
photo_urls: Mapped[list | None] = mapped_column(JSON)
status: Mapped[str] = mapped_column(String(24), default="published", server_default="published", index=True)
service_response: Mapped[str | None] = mapped_column(Text)
service_responded_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), index=True)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
service_center = relationship("ServiceCenter", back_populates="reviews")
comments = relationship("ServiceCenterReviewComment", back_populates="review", cascade="all, delete-orphan")
class ServiceCenterReviewComment(Base):
__tablename__ = "service_center_review_comments"
id: Mapped[int] = mapped_column(primary_key=True)
review_id: Mapped[int] = mapped_column(ForeignKey("service_center_reviews.id", ondelete="CASCADE"), index=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True)
text: Mapped[str] = mapped_column(Text)
status: Mapped[str] = mapped_column(String(24), default="published", server_default="published", index=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), index=True)
review = relationship("ServiceCenterReview", back_populates="comments")
class VehicleDataChangeRequest(Base):
__tablename__ = "vehicle_data_change_requests"

View File

@@ -2,7 +2,7 @@ import enum
from datetime import date, datetime
from decimal import Decimal
from sqlalchemy import Date, DateTime, Enum, ForeignKey, Numeric, String, Text, func
from sqlalchemy import Boolean, Date, DateTime, Enum, ForeignKey, Numeric, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base import Base
@@ -19,6 +19,29 @@ class ServiceType(str, enum.Enum):
other = "other"
class ExpenseCategory(str, enum.Enum):
insurance = "insurance"
tax = "tax"
fine = "fine"
parking = "parking"
car_wash = "car_wash"
toll = "toll"
tires = "tires"
wheels = "wheels"
battery = "battery"
parts = "parts"
repair = "repair"
maintenance = "maintenance"
diagnostics = "diagnostics"
towing = "towing"
loan_payment = "loan_payment"
loan_interest = "loan_interest"
state_fee = "state_fee"
registration = "registration"
inspection = "inspection"
other = "other"
class FuelEntry(Base):
__tablename__ = "fuel_entries"
@@ -56,3 +79,25 @@ class ServiceEntry(Base):
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
car = relationship("Car", back_populates="service_entries")
class ExpenseEntry(Base):
__tablename__ = "expense_entries"
id: Mapped[int] = mapped_column(primary_key=True)
car_id: Mapped[int] = mapped_column(ForeignKey("cars.id", ondelete="CASCADE"), index=True)
entry_date: Mapped[date] = mapped_column(Date, index=True)
category: Mapped[ExpenseCategory] = mapped_column(Enum(ExpenseCategory), index=True)
title: Mapped[str] = mapped_column(String(180))
vendor: Mapped[str | None] = mapped_column(String(160))
total_cost: Mapped[Decimal] = mapped_column(Numeric(12, 2))
currency: Mapped[str] = mapped_column(String(3), default="RUB", server_default="RUB")
odometer: Mapped[int | None]
period_start: Mapped[date | None] = mapped_column(Date)
period_end: Mapped[date | None] = mapped_column(Date)
period_months: Mapped[int | None]
is_recurring: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false")
notes: Mapped[str | None] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
car = relationship("Car", back_populates="expense_entries")