from datetime import UTC, datetime from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import ensure_service_employee, get_current_telegram_user, log_audit from app.db.session import get_session from app.models.car import ( Car, CarServiceLink, ServiceAppointment, ServiceProductItem, ServiceVisit, ServiceWorkItem, WorkOrderCorrection, WorkOrderStatusHistory, ) from app.models.user import User from app.schemas.service_center import ( ServiceProductItemCreate, ServiceProductItemRead, ServiceVisitRead, ServiceWorkItemCreate, ServiceWorkItemRead, WorkOrderCorrectionCreate, WorkOrderCorrectionRead, WorkOrderDecision, WorkOrderStatusHistoryRead, WorkOrderUpdate, ) from app.services.sto_booking import create_service_notification from app.services.work_orders import ( add_labor_item, add_product_item, add_status_history, assign_work_order_number, close_work_order, ensure_work_order_editable, refresh_work_order_totals, ) router = APIRouter(prefix="/work-orders", tags=["work-orders"]) async def get_work_order(session: AsyncSession, work_order_id: int) -> ServiceVisit: visit = await session.get(ServiceVisit, work_order_id) if visit is None: raise HTTPException(status_code=404, detail="Work order not found") return visit async def ensure_work_order_sto_access( session: AsyncSession, visit: ServiceVisit, user: User, allowed_roles: set[str] | None = None ) -> None: await ensure_service_employee(session, visit.service_center_id, user, allowed_roles) await ensure_work_order_vehicle_scope(session, visit) async def ensure_work_order_owner_access(session: AsyncSession, visit: ServiceVisit, user: User) -> Car: vehicle = await session.get(Car, visit.vehicle_id) if vehicle is None: raise HTTPException(status_code=404, detail="Vehicle not found") if vehicle.owner_id != user.id: raise HTTPException(status_code=403, detail="Forbidden") return vehicle async def ensure_work_order_vehicle_scope(session: AsyncSession, visit: ServiceVisit) -> None: link = ( await session.execute( select(CarServiceLink).where( CarServiceLink.car_id == visit.vehicle_id, CarServiceLink.service_center_id == visit.service_center_id, CarServiceLink.status == "approved", CarServiceLink.is_active.is_(True), ) ) ).scalar_one_or_none() if link is not None: return appointment = ( await session.execute( select(ServiceAppointment).where( ServiceAppointment.linked_work_order_id == visit.id, ServiceAppointment.service_center_id == visit.service_center_id, ServiceAppointment.vehicle_id == visit.vehicle_id, ServiceAppointment.status.in_(["converted_to_work_order", "completed"]), ) ) ).scalar_one_or_none() if appointment is None: raise HTTPException(status_code=403, detail="Vehicle access is not confirmed by owner") @router.get("/{work_order_id}", response_model=ServiceVisitRead) async def get_work_order_detail( work_order_id: int, session: AsyncSession = Depends(get_session), current_user: User = Depends(get_current_telegram_user), ) -> ServiceVisit: visit = await get_work_order(session, work_order_id) vehicle = await session.get(Car, visit.vehicle_id) if vehicle is None: raise HTTPException(status_code=404, detail="Vehicle not found") if vehicle.owner_id == current_user.id: return visit await ensure_work_order_sto_access(session, visit, current_user) return visit @router.patch("/{work_order_id}", response_model=ServiceVisitRead) async def update_work_order( work_order_id: int, payload: WorkOrderUpdate, session: AsyncSession = Depends(get_session), current_user: User = Depends(get_current_telegram_user), ) -> ServiceVisit: visit = await get_work_order(session, work_order_id) await ensure_work_order_sto_access(session, visit, current_user, {"owner", "manager", "receptionist", "mechanic"}) await ensure_work_order_editable(visit) for field, value in payload.model_dump(exclude_unset=True).items(): setattr(visit, field, value) await refresh_work_order_totals(session, visit) await log_audit(session, actor=current_user, action="work_order.update", target_type="service_visit", target_id=visit.id) await session.commit() await session.refresh(visit) return visit @router.post("/{work_order_id}/labor-items", response_model=ServiceWorkItemRead, status_code=status.HTTP_201_CREATED) async def create_labor_item( work_order_id: int, payload: ServiceWorkItemCreate, session: AsyncSession = Depends(get_session), current_user: User = Depends(get_current_telegram_user), ) -> ServiceWorkItem: visit = await get_work_order(session, work_order_id) await ensure_work_order_sto_access(session, visit, current_user, {"owner", "manager", "mechanic"}) item = await add_labor_item(session, visit, payload=payload.model_dump()) await log_audit(session, actor=current_user, action="work_order.labor_item.create", target_type="service_visit", target_id=visit.id) await session.commit() await session.refresh(item) return item @router.post("/{work_order_id}/product-items", response_model=ServiceProductItemRead, status_code=status.HTTP_201_CREATED) async def create_product_item( work_order_id: int, payload: ServiceProductItemCreate, session: AsyncSession = Depends(get_session), current_user: User = Depends(get_current_telegram_user), ) -> ServiceProductItem: visit = await get_work_order(session, work_order_id) await ensure_work_order_sto_access(session, visit, current_user, {"owner", "manager", "mechanic"}) item = await add_product_item(session, visit, payload=payload.model_dump()) await log_audit(session, actor=current_user, action="work_order.product_item.create", target_type="service_visit", target_id=visit.id) await session.commit() await session.refresh(item) return item @router.post("/{work_order_id}/submit-approval", response_model=ServiceVisitRead) async def submit_work_order_for_approval( work_order_id: int, payload: WorkOrderDecision, session: AsyncSession = Depends(get_session), current_user: User = Depends(get_current_telegram_user), ) -> ServiceVisit: visit = await get_work_order(session, work_order_id) await ensure_work_order_sto_access(session, visit, current_user, {"owner", "manager", "receptionist"}) await ensure_work_order_editable(visit) await assign_work_order_number(session, visit) await refresh_work_order_totals(session, visit) visit.approval_required = True await add_status_history(session, visit, to_status="waiting_owner_approval", actor=current_user, comment=payload.comment) vehicle = await session.get(Car, visit.vehicle_id) if vehicle is None: raise HTTPException(status_code=404, detail="Vehicle not found") await create_service_notification( session, recipient_user_id=vehicle.owner_id, service_center_id=visit.service_center_id, notification_type="work_order.waiting_owner_approval", title="Заказ-наряд ожидает согласования", body=f"{visit.work_order_number}: {visit.final_total} {visit.currency}", idempotency_key=f"work_order:{visit.id}:waiting_owner_approval:{visit.final_total}", ) await log_audit(session, actor=current_user, action="work_order.submit_approval", target_type="service_visit", target_id=visit.id) await session.commit() await session.refresh(visit) return visit @router.post("/{work_order_id}/approve", response_model=ServiceVisitRead) async def approve_work_order( work_order_id: int, payload: WorkOrderDecision, session: AsyncSession = Depends(get_session), current_user: User = Depends(get_current_telegram_user), ) -> ServiceVisit: visit = await get_work_order(session, work_order_id) await ensure_work_order_owner_access(session, visit, current_user) if visit.status != "waiting_owner_approval": raise HTTPException(status_code=409, detail="Work order is not waiting for owner approval") await refresh_work_order_totals(session, visit) visit.price_approved_total = visit.final_total visit.approved_at = datetime.now(UTC) visit.owner_resolved_at = visit.approved_at visit.owner_comment = payload.comment await add_status_history(session, visit, to_status="approved_by_owner", actor=current_user, comment=payload.comment) await create_service_notification( session, recipient_user_id=visit.owner_id or current_user.id, service_center_id=visit.service_center_id, notification_type="work_order.approved_by_owner", title="Заказ-наряд согласован", body=visit.work_order_number, send_telegram=False, idempotency_key=f"work_order:{visit.id}:approved_by_owner", ) await log_audit(session, actor=current_user, action="work_order.approve", target_type="service_visit", target_id=visit.id) await session.commit() await session.refresh(visit) return visit @router.post("/{work_order_id}/reject", response_model=ServiceVisitRead) async def reject_work_order( work_order_id: int, payload: WorkOrderDecision, session: AsyncSession = Depends(get_session), current_user: User = Depends(get_current_telegram_user), ) -> ServiceVisit: visit = await get_work_order(session, work_order_id) await ensure_work_order_owner_access(session, visit, current_user) if visit.status != "waiting_owner_approval": raise HTTPException(status_code=409, detail="Work order is not waiting for owner approval") visit.owner_comment = payload.comment visit.owner_resolved_at = datetime.now(UTC) await add_status_history(session, visit, to_status="rejected_by_owner", actor=current_user, comment=payload.comment) await log_audit(session, actor=current_user, action="work_order.reject", target_type="service_visit", target_id=visit.id) await session.commit() await session.refresh(visit) return visit @router.post("/{work_order_id}/start", response_model=ServiceVisitRead) async def start_work_order( work_order_id: int, payload: WorkOrderDecision, session: AsyncSession = Depends(get_session), current_user: User = Depends(get_current_telegram_user), ) -> ServiceVisit: visit = await get_work_order(session, work_order_id) await ensure_work_order_sto_access(session, visit, current_user, {"owner", "manager", "mechanic"}) if visit.status not in {"draft", "diagnosis", "approved_by_owner"}: raise HTTPException(status_code=409, detail="Work order cannot be started") await add_status_history(session, visit, to_status="in_progress", actor=current_user, comment=payload.comment) await log_audit(session, actor=current_user, action="work_order.start", target_type="service_visit", target_id=visit.id) await session.commit() await session.refresh(visit) return visit @router.post("/{work_order_id}/complete", response_model=ServiceVisitRead) async def complete_work_order( work_order_id: int, payload: WorkOrderDecision, session: AsyncSession = Depends(get_session), current_user: User = Depends(get_current_telegram_user), ) -> ServiceVisit: visit = await get_work_order(session, work_order_id) await ensure_work_order_sto_access(session, visit, current_user, {"owner", "manager"}) await close_work_order( session, visit, actor=current_user, confirm_lower_odometer=payload.confirm_lower_odometer, ) await log_audit(session, actor=current_user, action="work_order.complete", target_type="service_visit", target_id=visit.id) await session.commit() await session.refresh(visit) return visit @router.post("/{work_order_id}/corrections", response_model=WorkOrderCorrectionRead, status_code=status.HTTP_201_CREATED) async def create_work_order_correction( work_order_id: int, payload: WorkOrderCorrectionCreate, session: AsyncSession = Depends(get_session), current_user: User = Depends(get_current_telegram_user), ) -> WorkOrderCorrection: visit = await get_work_order(session, work_order_id) await ensure_work_order_sto_access(session, visit, current_user, {"owner", "manager"}) if visit.status != "completed": raise HTTPException(status_code=409, detail="Correction flow is only for completed work orders") correction = WorkOrderCorrection( service_visit_id=visit.id, requested_by_user_id=current_user.id, reason=payload.reason, proposed_changes=payload.proposed_changes, owner_approval_required=payload.owner_approval_required, created_version=visit.version or 1, ) session.add(correction) await log_audit( session, actor=current_user, action="work_order.correction.create", target_type="service_visit", target_id=visit.id, metadata={"reason": payload.reason}, ) await session.commit() await session.refresh(correction) return correction @router.get("/{work_order_id}/status-history", response_model=list[WorkOrderStatusHistoryRead]) async def work_order_status_history( work_order_id: int, session: AsyncSession = Depends(get_session), current_user: User = Depends(get_current_telegram_user), ) -> list[WorkOrderStatusHistory]: visit = await get_work_order(session, work_order_id) vehicle = await session.get(Car, visit.vehicle_id) if vehicle is None: raise HTTPException(status_code=404, detail="Vehicle not found") if vehicle.owner_id != current_user.id: await ensure_work_order_sto_access(session, visit, current_user) result = await session.execute( select(WorkOrderStatusHistory) .where(WorkOrderStatusHistory.service_visit_id == visit.id) .order_by(WorkOrderStatusHistory.created_at.asc(), WorkOrderStatusHistory.id.asc()) ) return list(result.scalars())