Add service platform foundation
This commit is contained in:
@@ -2,35 +2,93 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.api.deps import require_internal_api_token
|
||||
from app.api.deps import (
|
||||
ensure_service_employee,
|
||||
get_current_telegram_user,
|
||||
get_or_create_telegram_user,
|
||||
log_audit,
|
||||
require_internal_api_token,
|
||||
)
|
||||
from app.db.session import get_session
|
||||
from app.models.car import Car, CarServiceLink, ServiceCenter, ServiceInboxMessage
|
||||
from app.models.car import (
|
||||
Car,
|
||||
CarServiceLink,
|
||||
ServiceCenter,
|
||||
ServiceCenterVerification,
|
||||
ServiceEmployee,
|
||||
ServiceInboxMessage,
|
||||
ServiceVisit,
|
||||
)
|
||||
from app.models.user import User
|
||||
from app.schemas.service_center import (
|
||||
CarServiceLinkCreate,
|
||||
CarServiceLinkRead,
|
||||
ServiceCenterCreate,
|
||||
ServiceCenterRead,
|
||||
ServiceCenterVerificationCreate,
|
||||
ServiceCenterVerificationRead,
|
||||
ServiceEmployeeInvite,
|
||||
ServiceEmployeeRead,
|
||||
ServiceInboxCreate,
|
||||
ServiceInboxRead,
|
||||
ServiceVisitCreate,
|
||||
ServiceVisitRead,
|
||||
VehicleSearchRequest,
|
||||
VehicleSearchResult,
|
||||
)
|
||||
from app.services.vehicle_identity import mask_license_plate, mask_vin
|
||||
|
||||
router = APIRouter(prefix="/service-centers", tags=["service-centers"])
|
||||
|
||||
|
||||
@router.post("", response_model=ServiceCenterRead, status_code=status.HTTP_201_CREATED)
|
||||
async def create_service_center(
|
||||
payload: ServiceCenterCreate,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
x_internal_api_token: str | None = Header(default=None, alias="X-Internal-API-Token"),
|
||||
current_user: User = Depends(get_current_telegram_user),
|
||||
) -> ServiceCenter:
|
||||
require_internal_api_token(x_internal_api_token)
|
||||
center = ServiceCenter(**payload.model_dump())
|
||||
center = ServiceCenter(
|
||||
name=payload.display_name,
|
||||
display_name=payload.display_name,
|
||||
legal_name=payload.legal_name,
|
||||
country=payload.country.upper() if payload.country else None,
|
||||
city=payload.city,
|
||||
address=payload.address,
|
||||
phone=payload.phone,
|
||||
contact_phone=payload.contact_phone or payload.phone,
|
||||
telegram_chat_id=payload.telegram_chat_id,
|
||||
business_registration_number=payload.business_registration_number,
|
||||
owner_user_id=current_user.id,
|
||||
verification_status="pending",
|
||||
)
|
||||
session.add(center)
|
||||
await session.flush()
|
||||
employee = ServiceEmployee(
|
||||
service_center_id=center.id,
|
||||
user_id=current_user.id,
|
||||
role="owner",
|
||||
status="active",
|
||||
)
|
||||
session.add(employee)
|
||||
await log_audit(session, actor=current_user, action="service_center.create", target_type="service_center", target_id=center.id)
|
||||
await session.commit()
|
||||
await session.refresh(center)
|
||||
return center
|
||||
|
||||
|
||||
@router.get("/my", response_model=list[ServiceCenterRead])
|
||||
async def my_service_centers(
|
||||
session: AsyncSession = Depends(get_session),
|
||||
current_user: User = Depends(get_current_telegram_user),
|
||||
) -> list[ServiceCenter]:
|
||||
result = await session.execute(
|
||||
select(ServiceCenter)
|
||||
.join(ServiceEmployee, ServiceEmployee.service_center_id == ServiceCenter.id)
|
||||
.where(ServiceEmployee.user_id == current_user.id, ServiceEmployee.status == "active")
|
||||
.order_by(ServiceCenter.created_at.desc())
|
||||
)
|
||||
return list(result.scalars())
|
||||
|
||||
|
||||
@router.get("", response_model=list[ServiceCenterRead])
|
||||
async def list_service_centers(
|
||||
session: AsyncSession = Depends(get_session),
|
||||
@@ -41,6 +99,152 @@ async def list_service_centers(
|
||||
return list(result.scalars())
|
||||
|
||||
|
||||
@router.post("/{service_center_id}/verification", response_model=ServiceCenterVerificationRead)
|
||||
async def submit_verification(
|
||||
service_center_id: int,
|
||||
payload: ServiceCenterVerificationCreate,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
current_user: User = Depends(get_current_telegram_user),
|
||||
) -> ServiceCenterVerification:
|
||||
await ensure_service_employee(session, service_center_id, current_user, {"owner", "manager"})
|
||||
verification = ServiceCenterVerification(
|
||||
service_center_id=service_center_id,
|
||||
submitted_documents=payload.submitted_documents,
|
||||
comment=payload.comment,
|
||||
status="pending",
|
||||
)
|
||||
session.add(verification)
|
||||
center = await session.get(ServiceCenter, service_center_id)
|
||||
if center:
|
||||
center.verification_status = "pending"
|
||||
await log_audit(session, actor=current_user, action="service_center.verification.submit", target_type="service_center", target_id=service_center_id)
|
||||
await session.commit()
|
||||
await session.refresh(verification)
|
||||
return verification
|
||||
|
||||
|
||||
@router.post("/{service_center_id}/employees/invite", response_model=ServiceEmployeeRead)
|
||||
async def invite_employee(
|
||||
service_center_id: int,
|
||||
payload: ServiceEmployeeInvite,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
current_user: User = Depends(get_current_telegram_user),
|
||||
) -> ServiceEmployee:
|
||||
await ensure_service_employee(session, service_center_id, current_user, {"owner", "manager"})
|
||||
user = await get_or_create_telegram_user(session, telegram_id=payload.telegram_id)
|
||||
result = await session.execute(
|
||||
select(ServiceEmployee).where(
|
||||
ServiceEmployee.service_center_id == service_center_id,
|
||||
ServiceEmployee.user_id == user.id,
|
||||
)
|
||||
)
|
||||
employee = result.scalar_one_or_none()
|
||||
if employee is None:
|
||||
employee = ServiceEmployee(
|
||||
service_center_id=service_center_id,
|
||||
user_id=user.id,
|
||||
role=payload.role,
|
||||
permissions=payload.permissions,
|
||||
status="invited",
|
||||
)
|
||||
session.add(employee)
|
||||
else:
|
||||
employee.role = payload.role
|
||||
employee.permissions = payload.permissions
|
||||
employee.status = "invited"
|
||||
await log_audit(session, actor=current_user, action="service_employee.invite", target_type="service_center", target_id=service_center_id, metadata={"telegram_id": payload.telegram_id})
|
||||
await session.commit()
|
||||
await session.refresh(employee)
|
||||
return employee
|
||||
|
||||
|
||||
@router.get("/{service_center_id}/visits", response_model=list[ServiceVisitRead])
|
||||
async def service_center_visits(
|
||||
service_center_id: int,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
current_user: User = Depends(get_current_telegram_user),
|
||||
) -> list[ServiceVisit]:
|
||||
await ensure_service_employee(session, service_center_id, current_user)
|
||||
result = await session.execute(
|
||||
select(ServiceVisit)
|
||||
.where(ServiceVisit.service_center_id == service_center_id)
|
||||
.order_by(ServiceVisit.visit_date.desc(), ServiceVisit.id.desc())
|
||||
)
|
||||
return list(result.scalars())
|
||||
|
||||
|
||||
@router.post("/{service_center_id}/visits", response_model=ServiceVisitRead, status_code=status.HTTP_201_CREATED)
|
||||
async def create_visit(
|
||||
service_center_id: int,
|
||||
payload: ServiceVisitCreate,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
current_user: User = Depends(get_current_telegram_user),
|
||||
) -> ServiceVisit:
|
||||
employee = await ensure_service_employee(session, service_center_id, current_user, {"owner", "manager", "receptionist"})
|
||||
vehicle = await session.get(Car, payload.vehicle_id)
|
||||
if vehicle is None:
|
||||
raise HTTPException(status_code=404, detail="Vehicle not found")
|
||||
center = await session.get(ServiceCenter, service_center_id)
|
||||
if center and center.verification_status not in {"verified", "pending"}:
|
||||
raise HTTPException(status_code=403, detail="Service center is not allowed to create visits")
|
||||
visit = ServiceVisit(
|
||||
service_center_id=service_center_id,
|
||||
vehicle_id=payload.vehicle_id,
|
||||
created_by_employee_id=employee.id,
|
||||
visit_date=payload.visit_date,
|
||||
odometer=payload.odometer,
|
||||
notes=payload.notes,
|
||||
total_cost=payload.total_cost,
|
||||
currency=payload.currency,
|
||||
status="draft",
|
||||
)
|
||||
session.add(visit)
|
||||
await log_audit(session, actor=current_user, action="service_visit.create", target_type="service_visit", metadata={"vehicle_id": payload.vehicle_id})
|
||||
await session.commit()
|
||||
await session.refresh(visit)
|
||||
return visit
|
||||
|
||||
|
||||
@router.post("/{service_center_id}/vehicle-access/request")
|
||||
async def request_vehicle_access(
|
||||
service_center_id: int,
|
||||
payload: VehicleSearchRequest,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
current_user: User = Depends(get_current_telegram_user),
|
||||
) -> VehicleSearchResult:
|
||||
await ensure_service_employee(session, service_center_id, current_user, {"owner", "manager", "receptionist"})
|
||||
stmt = select(Car)
|
||||
if payload.vin:
|
||||
stmt = stmt.where(Car.vin_normalized == payload.vin)
|
||||
elif payload.license_plate:
|
||||
stmt = stmt.where(Car.license_plate_normalized == payload.license_plate)
|
||||
if payload.country_code:
|
||||
stmt = stmt.where(Car.license_plate_country == payload.country_code.upper())
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="license_plate or vin is required")
|
||||
vehicle = (await session.execute(stmt.limit(1))).scalar_one_or_none()
|
||||
await log_audit(
|
||||
session,
|
||||
actor=current_user,
|
||||
action="vehicle_access.request",
|
||||
target_type="vehicle",
|
||||
target_id=vehicle.id if vehicle else None,
|
||||
metadata={"service_center_id": service_center_id, "found": bool(vehicle)},
|
||||
)
|
||||
await session.commit()
|
||||
if vehicle is None:
|
||||
return VehicleSearchResult(access_status="not_found")
|
||||
return VehicleSearchResult(
|
||||
vehicle_id=vehicle.id,
|
||||
make=vehicle.make,
|
||||
model=vehicle.model,
|
||||
year=vehicle.year,
|
||||
masked_license_plate=mask_license_plate(vehicle.license_plate_display or vehicle.plate_number),
|
||||
masked_vin=mask_vin(vehicle.vin_normalized or vehicle.vin),
|
||||
access_status="request_logged",
|
||||
)
|
||||
|
||||
|
||||
@router.post("/links", response_model=CarServiceLinkRead, status_code=status.HTTP_201_CREATED)
|
||||
async def link_car_to_service(
|
||||
payload: CarServiceLinkCreate,
|
||||
|
||||
Reference in New Issue
Block a user