Add service platform foundation
This commit is contained in:
303
alembic/versions/202605120005_platform_service_visits.py
Normal file
303
alembic/versions/202605120005_platform_service_visits.py
Normal file
@@ -0,0 +1,303 @@
|
||||
"""platform service visits
|
||||
|
||||
Revision ID: 202605120005
|
||||
Revises: 202605120004
|
||||
Create Date: 2026-05-12
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "202605120005"
|
||||
down_revision: str | None = "202605120004"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column("users", sa.Column("platform_role", sa.String(length=32), server_default="user", nullable=False))
|
||||
op.create_index(op.f("ix_users_platform_role"), "users", ["platform_role"])
|
||||
|
||||
op.add_column("cars", sa.Column("license_plate_display", sa.String(length=32), nullable=True))
|
||||
op.add_column("cars", sa.Column("license_plate_normalized", sa.String(length=32), nullable=True))
|
||||
op.add_column("cars", sa.Column("license_plate_country", sa.String(length=2), nullable=True))
|
||||
op.add_column("cars", sa.Column("vin_normalized", sa.String(length=17), nullable=True))
|
||||
op.execute(
|
||||
"""
|
||||
update cars
|
||||
set license_plate_display = plate_number,
|
||||
license_plate_normalized = upper(replace(replace(plate_number, ' ', ''), '-', '')),
|
||||
vin_normalized = upper(vin)
|
||||
where plate_number is not null or vin is not null
|
||||
"""
|
||||
)
|
||||
op.create_index(op.f("ix_cars_license_plate_normalized"), "cars", ["license_plate_normalized"])
|
||||
op.create_index(op.f("ix_cars_license_plate_country"), "cars", ["license_plate_country"])
|
||||
op.create_index(op.f("ix_cars_vin_normalized"), "cars", ["vin_normalized"], unique=True)
|
||||
op.create_index(
|
||||
"uq_cars_country_license_plate",
|
||||
"cars",
|
||||
["license_plate_country", "license_plate_normalized"],
|
||||
unique=True,
|
||||
postgresql_where=sa.text("license_plate_country is not null and license_plate_normalized is not null"),
|
||||
)
|
||||
|
||||
op.add_column("service_centers", sa.Column("legal_name", sa.String(length=240), nullable=True))
|
||||
op.add_column("service_centers", sa.Column("display_name", sa.String(length=160), nullable=True))
|
||||
op.add_column("service_centers", sa.Column("country", sa.String(length=2), nullable=True))
|
||||
op.add_column("service_centers", sa.Column("city", sa.String(length=120), nullable=True))
|
||||
op.add_column("service_centers", sa.Column("phone", sa.String(length=40), nullable=True))
|
||||
op.add_column("service_centers", sa.Column("business_registration_number", sa.String(length=80), nullable=True))
|
||||
op.add_column(
|
||||
"service_centers",
|
||||
sa.Column("verification_status", sa.String(length=24), server_default="pending", nullable=False),
|
||||
)
|
||||
op.add_column("service_centers", sa.Column("owner_user_id", sa.Integer(), nullable=True))
|
||||
op.add_column("service_centers", sa.Column("verified_at", sa.DateTime(timezone=True), nullable=True))
|
||||
op.add_column("service_centers", sa.Column("suspended_at", sa.DateTime(timezone=True), nullable=True))
|
||||
op.create_foreign_key(
|
||||
"fk_service_centers_owner_user_id_users",
|
||||
"service_centers",
|
||||
"users",
|
||||
["owner_user_id"],
|
||||
["id"],
|
||||
ondelete="SET NULL",
|
||||
)
|
||||
op.create_index(op.f("ix_service_centers_display_name"), "service_centers", ["display_name"])
|
||||
op.create_index(op.f("ix_service_centers_country"), "service_centers", ["country"])
|
||||
op.create_index(op.f("ix_service_centers_verification_status"), "service_centers", ["verification_status"])
|
||||
op.create_index(op.f("ix_service_centers_owner_user_id"), "service_centers", ["owner_user_id"])
|
||||
op.execute("update service_centers set display_name = name where display_name is null")
|
||||
|
||||
op.create_table(
|
||||
"vehicle_access",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("vehicle_id", sa.Integer(), nullable=False),
|
||||
sa.Column("user_id", sa.Integer(), nullable=False),
|
||||
sa.Column("role", sa.String(length=24), server_default="owner", nullable=False),
|
||||
sa.Column("status", sa.String(length=24), server_default="active", nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("revoked_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(["vehicle_id"], ["cars.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("vehicle_id", "user_id", "role", name="uq_vehicle_access_user_role"),
|
||||
)
|
||||
op.create_index(op.f("ix_vehicle_access_vehicle_id"), "vehicle_access", ["vehicle_id"])
|
||||
op.create_index(op.f("ix_vehicle_access_user_id"), "vehicle_access", ["user_id"])
|
||||
op.create_index(op.f("ix_vehicle_access_role"), "vehicle_access", ["role"])
|
||||
op.create_index(op.f("ix_vehicle_access_status"), "vehicle_access", ["status"])
|
||||
op.execute(
|
||||
"""
|
||||
insert into vehicle_access (vehicle_id, user_id, role, status)
|
||||
select id, owner_id, 'owner', 'active'
|
||||
from cars
|
||||
on conflict do nothing
|
||||
"""
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"service_center_verifications",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("service_center_id", sa.Integer(), nullable=False),
|
||||
sa.Column("submitted_documents", sa.JSON(), nullable=True),
|
||||
sa.Column("comment", sa.Text(), nullable=True),
|
||||
sa.Column("status", sa.String(length=24), server_default="pending", nullable=False),
|
||||
sa.Column("reviewed_by", sa.Integer(), nullable=True),
|
||||
sa.Column("reviewed_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["reviewed_by"], ["users.id"], ondelete="SET NULL"),
|
||||
sa.ForeignKeyConstraint(["service_center_id"], ["service_centers.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_service_center_verifications_service_center_id"), "service_center_verifications", ["service_center_id"])
|
||||
op.create_index(op.f("ix_service_center_verifications_status"), "service_center_verifications", ["status"])
|
||||
op.create_index(op.f("ix_service_center_verifications_reviewed_by"), "service_center_verifications", ["reviewed_by"])
|
||||
|
||||
op.create_table(
|
||||
"service_employees",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("service_center_id", sa.Integer(), nullable=False),
|
||||
sa.Column("user_id", sa.Integer(), nullable=False),
|
||||
sa.Column("role", sa.String(length=32), server_default="receptionist", nullable=False),
|
||||
sa.Column("permissions", sa.JSON(), nullable=True),
|
||||
sa.Column("status", sa.String(length=24), server_default="active", nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["service_center_id"], ["service_centers.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("service_center_id", "user_id", name="uq_service_employee_user"),
|
||||
)
|
||||
op.create_index(op.f("ix_service_employees_service_center_id"), "service_employees", ["service_center_id"])
|
||||
op.create_index(op.f("ix_service_employees_user_id"), "service_employees", ["user_id"])
|
||||
op.create_index(op.f("ix_service_employees_role"), "service_employees", ["role"])
|
||||
op.create_index(op.f("ix_service_employees_status"), "service_employees", ["status"])
|
||||
|
||||
op.create_table(
|
||||
"service_visits",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("service_center_id", sa.Integer(), nullable=False),
|
||||
sa.Column("vehicle_id", sa.Integer(), nullable=False),
|
||||
sa.Column("created_by_employee_id", sa.Integer(), nullable=True),
|
||||
sa.Column("visit_date", sa.Date(), nullable=False),
|
||||
sa.Column("odometer", sa.Integer(), nullable=True),
|
||||
sa.Column("status", sa.String(length=40), server_default="draft", nullable=False),
|
||||
sa.Column("notes", sa.Text(), nullable=True),
|
||||
sa.Column("total_cost", sa.Numeric(12, 2), nullable=True),
|
||||
sa.Column("currency", sa.String(length=3), server_default="RUB", nullable=False),
|
||||
sa.Column("owner_resolved_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["created_by_employee_id"], ["service_employees.id"], ondelete="SET NULL"),
|
||||
sa.ForeignKeyConstraint(["service_center_id"], ["service_centers.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(["vehicle_id"], ["cars.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_service_visits_service_center_id"), "service_visits", ["service_center_id"])
|
||||
op.create_index(op.f("ix_service_visits_vehicle_id"), "service_visits", ["vehicle_id"])
|
||||
op.create_index(op.f("ix_service_visits_created_by_employee_id"), "service_visits", ["created_by_employee_id"])
|
||||
op.create_index(op.f("ix_service_visits_visit_date"), "service_visits", ["visit_date"])
|
||||
op.create_index(op.f("ix_service_visits_status"), "service_visits", ["status"])
|
||||
|
||||
op.create_table(
|
||||
"service_work_items",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("service_visit_id", sa.Integer(), nullable=False),
|
||||
sa.Column("work_type", sa.String(length=40), server_default="other", nullable=False),
|
||||
sa.Column("title", sa.String(length=180), nullable=False),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("parts", sa.JSON(), nullable=True),
|
||||
sa.Column("oil_brand", sa.String(length=80), nullable=True),
|
||||
sa.Column("oil_viscosity", sa.String(length=40), nullable=True),
|
||||
sa.Column("oil_volume", sa.Numeric(5, 2), nullable=True),
|
||||
sa.Column("next_due_odometer", sa.Integer(), nullable=True),
|
||||
sa.Column("next_due_date", sa.Date(), nullable=True),
|
||||
sa.Column("price", sa.Numeric(12, 2), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["service_visit_id"], ["service_visits.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_service_work_items_service_visit_id"), "service_work_items", ["service_visit_id"])
|
||||
op.create_index(op.f("ix_service_work_items_work_type"), "service_work_items", ["work_type"])
|
||||
|
||||
op.create_table(
|
||||
"vehicle_data_change_requests",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("vehicle_id", sa.Integer(), nullable=False),
|
||||
sa.Column("requested_by_service_center_id", sa.Integer(), nullable=True),
|
||||
sa.Column("requested_by_employee_id", sa.Integer(), nullable=True),
|
||||
sa.Column("field_name", sa.String(length=80), nullable=False),
|
||||
sa.Column("old_value", sa.Text(), nullable=True),
|
||||
sa.Column("new_value", sa.Text(), nullable=True),
|
||||
sa.Column("status", sa.String(length=24), server_default="pending", nullable=False),
|
||||
sa.Column("owner_user_id", sa.Integer(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
||||
sa.Column("resolved_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(["owner_user_id"], ["users.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(["requested_by_employee_id"], ["service_employees.id"], ondelete="SET NULL"),
|
||||
sa.ForeignKeyConstraint(["requested_by_service_center_id"], ["service_centers.id"], ondelete="SET NULL"),
|
||||
sa.ForeignKeyConstraint(["vehicle_id"], ["cars.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_vehicle_data_change_requests_vehicle_id"), "vehicle_data_change_requests", ["vehicle_id"])
|
||||
op.create_index(op.f("ix_vehicle_data_change_requests_requested_by_service_center_id"), "vehicle_data_change_requests", ["requested_by_service_center_id"])
|
||||
op.create_index(op.f("ix_vehicle_data_change_requests_requested_by_employee_id"), "vehicle_data_change_requests", ["requested_by_employee_id"])
|
||||
op.create_index(op.f("ix_vehicle_data_change_requests_field_name"), "vehicle_data_change_requests", ["field_name"])
|
||||
op.create_index(op.f("ix_vehicle_data_change_requests_status"), "vehicle_data_change_requests", ["status"])
|
||||
op.create_index(op.f("ix_vehicle_data_change_requests_owner_user_id"), "vehicle_data_change_requests", ["owner_user_id"])
|
||||
|
||||
op.create_table(
|
||||
"audit_logs",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("actor_user_id", sa.Integer(), nullable=True),
|
||||
sa.Column("actor_role", sa.String(length=64), nullable=True),
|
||||
sa.Column("action", sa.String(length=120), nullable=False),
|
||||
sa.Column("target_type", sa.String(length=80), nullable=False),
|
||||
sa.Column("target_id", sa.String(length=80), nullable=True),
|
||||
sa.Column("ip", sa.String(length=64), nullable=True),
|
||||
sa.Column("user_agent", sa.String(length=256), nullable=True),
|
||||
sa.Column("metadata_json", sa.JSON(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["actor_user_id"], ["users.id"], ondelete="SET NULL"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_audit_logs_actor_user_id"), "audit_logs", ["actor_user_id"])
|
||||
op.create_index(op.f("ix_audit_logs_action"), "audit_logs", ["action"])
|
||||
op.create_index(op.f("ix_audit_logs_target_type"), "audit_logs", ["target_type"])
|
||||
op.create_index(op.f("ix_audit_logs_target_id"), "audit_logs", ["target_id"])
|
||||
op.create_index(op.f("ix_audit_logs_created_at"), "audit_logs", ["created_at"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index(op.f("ix_audit_logs_created_at"), table_name="audit_logs")
|
||||
op.drop_index(op.f("ix_audit_logs_target_id"), table_name="audit_logs")
|
||||
op.drop_index(op.f("ix_audit_logs_target_type"), table_name="audit_logs")
|
||||
op.drop_index(op.f("ix_audit_logs_action"), table_name="audit_logs")
|
||||
op.drop_index(op.f("ix_audit_logs_actor_user_id"), table_name="audit_logs")
|
||||
op.drop_table("audit_logs")
|
||||
|
||||
op.drop_index(op.f("ix_vehicle_data_change_requests_owner_user_id"), table_name="vehicle_data_change_requests")
|
||||
op.drop_index(op.f("ix_vehicle_data_change_requests_status"), table_name="vehicle_data_change_requests")
|
||||
op.drop_index(op.f("ix_vehicle_data_change_requests_field_name"), table_name="vehicle_data_change_requests")
|
||||
op.drop_index(op.f("ix_vehicle_data_change_requests_requested_by_employee_id"), table_name="vehicle_data_change_requests")
|
||||
op.drop_index(op.f("ix_vehicle_data_change_requests_requested_by_service_center_id"), table_name="vehicle_data_change_requests")
|
||||
op.drop_index(op.f("ix_vehicle_data_change_requests_vehicle_id"), table_name="vehicle_data_change_requests")
|
||||
op.drop_table("vehicle_data_change_requests")
|
||||
|
||||
op.drop_index(op.f("ix_service_work_items_work_type"), table_name="service_work_items")
|
||||
op.drop_index(op.f("ix_service_work_items_service_visit_id"), table_name="service_work_items")
|
||||
op.drop_table("service_work_items")
|
||||
|
||||
op.drop_index(op.f("ix_service_visits_status"), table_name="service_visits")
|
||||
op.drop_index(op.f("ix_service_visits_visit_date"), table_name="service_visits")
|
||||
op.drop_index(op.f("ix_service_visits_created_by_employee_id"), table_name="service_visits")
|
||||
op.drop_index(op.f("ix_service_visits_vehicle_id"), table_name="service_visits")
|
||||
op.drop_index(op.f("ix_service_visits_service_center_id"), table_name="service_visits")
|
||||
op.drop_table("service_visits")
|
||||
|
||||
op.drop_index(op.f("ix_service_employees_status"), table_name="service_employees")
|
||||
op.drop_index(op.f("ix_service_employees_role"), table_name="service_employees")
|
||||
op.drop_index(op.f("ix_service_employees_user_id"), table_name="service_employees")
|
||||
op.drop_index(op.f("ix_service_employees_service_center_id"), table_name="service_employees")
|
||||
op.drop_table("service_employees")
|
||||
|
||||
op.drop_index(op.f("ix_service_center_verifications_reviewed_by"), table_name="service_center_verifications")
|
||||
op.drop_index(op.f("ix_service_center_verifications_status"), table_name="service_center_verifications")
|
||||
op.drop_index(op.f("ix_service_center_verifications_service_center_id"), table_name="service_center_verifications")
|
||||
op.drop_table("service_center_verifications")
|
||||
|
||||
op.drop_index(op.f("ix_vehicle_access_status"), table_name="vehicle_access")
|
||||
op.drop_index(op.f("ix_vehicle_access_role"), table_name="vehicle_access")
|
||||
op.drop_index(op.f("ix_vehicle_access_user_id"), table_name="vehicle_access")
|
||||
op.drop_index(op.f("ix_vehicle_access_vehicle_id"), table_name="vehicle_access")
|
||||
op.drop_table("vehicle_access")
|
||||
|
||||
op.drop_index(op.f("ix_service_centers_owner_user_id"), table_name="service_centers")
|
||||
op.drop_index(op.f("ix_service_centers_verification_status"), table_name="service_centers")
|
||||
op.drop_index(op.f("ix_service_centers_country"), table_name="service_centers")
|
||||
op.drop_index(op.f("ix_service_centers_display_name"), table_name="service_centers")
|
||||
op.drop_constraint("fk_service_centers_owner_user_id_users", "service_centers", type_="foreignkey")
|
||||
op.drop_column("service_centers", "suspended_at")
|
||||
op.drop_column("service_centers", "verified_at")
|
||||
op.drop_column("service_centers", "owner_user_id")
|
||||
op.drop_column("service_centers", "verification_status")
|
||||
op.drop_column("service_centers", "business_registration_number")
|
||||
op.drop_column("service_centers", "phone")
|
||||
op.drop_column("service_centers", "city")
|
||||
op.drop_column("service_centers", "country")
|
||||
op.drop_column("service_centers", "display_name")
|
||||
op.drop_column("service_centers", "legal_name")
|
||||
|
||||
op.drop_index("uq_cars_country_license_plate", table_name="cars")
|
||||
op.drop_index(op.f("ix_cars_vin_normalized"), table_name="cars")
|
||||
op.drop_index(op.f("ix_cars_license_plate_country"), table_name="cars")
|
||||
op.drop_index(op.f("ix_cars_license_plate_normalized"), table_name="cars")
|
||||
op.drop_column("cars", "vin_normalized")
|
||||
op.drop_column("cars", "license_plate_country")
|
||||
op.drop_column("cars", "license_plate_normalized")
|
||||
op.drop_column("cars", "license_plate_display")
|
||||
|
||||
op.drop_index(op.f("ix_users_platform_role"), table_name="users")
|
||||
op.drop_column("users", "platform_role")
|
||||
Reference in New Issue
Block a user