This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
from datetime import UTC, date, datetime, timedelta
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -36,6 +36,7 @@ from app.schemas.sto_booking import (
|
||||
ServiceCenterHolidayRead,
|
||||
STODashboardRead,
|
||||
)
|
||||
from app.services.rate_limit import check_rate_limit
|
||||
from app.services.sto_booking import (
|
||||
calculate_available_slots,
|
||||
create_service_notification,
|
||||
@@ -46,6 +47,7 @@ from app.services.sto_booking import (
|
||||
money_to_float,
|
||||
notify_service_staff,
|
||||
)
|
||||
from app.services.work_orders import add_status_history, assign_work_order_number
|
||||
|
||||
APPROVED_SERVICE_STATUSES = {"verified", "approved"}
|
||||
|
||||
@@ -173,9 +175,11 @@ async def get_available_slots(
|
||||
@router.post("/appointments", response_model=AppointmentRead, status_code=status.HTTP_201_CREATED)
|
||||
async def create_appointment(
|
||||
payload: AppointmentCreate,
|
||||
request: Request,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
current_user: User = Depends(get_current_telegram_user),
|
||||
) -> ServiceAppointment:
|
||||
await check_rate_limit(scope="appointment_create", limit=20, window_seconds=3600, request=request, user=current_user, session=session)
|
||||
await _approved_service_center(session, payload.service_center_id)
|
||||
vehicle = await _owned_vehicle(session, payload.vehicle_id, current_user)
|
||||
duration = estimate_duration(payload.service_type, payload.estimated_duration_minutes)
|
||||
@@ -253,15 +257,15 @@ async def cancel_appointment(
|
||||
current_user: User = Depends(get_current_telegram_user),
|
||||
) -> ServiceAppointment:
|
||||
appointment = await _appointment_for_owner(session, appointment_id, current_user)
|
||||
if appointment.status in {"completed", "cancelled_by_customer", "cancelled_by_sto"}:
|
||||
if appointment.status in {"completed", "cancelled_by_owner", "cancelled_by_customer", "cancelled_by_sto"}:
|
||||
raise HTTPException(status_code=409, detail="Appointment cannot be cancelled")
|
||||
appointment.status = "cancelled_by_customer"
|
||||
appointment.status = "cancelled_by_owner"
|
||||
appointment.cancelled_at = datetime.now(UTC)
|
||||
appointment.cancellation_reason = payload.reason
|
||||
await notify_service_staff(
|
||||
session,
|
||||
service_center_id=appointment.service_center_id,
|
||||
notification_type="appointment.cancelled_by_customer",
|
||||
notification_type="appointment.cancelled_by_owner",
|
||||
title="Клиент отменил запись",
|
||||
body=payload.reason,
|
||||
appointment_id=appointment.id,
|
||||
@@ -316,7 +320,7 @@ async def reject_proposed_time(
|
||||
appointment = await _appointment_for_owner(session, appointment_id, current_user)
|
||||
if appointment.status != "proposed_new_time":
|
||||
raise HTTPException(status_code=409, detail="Appointment has no proposed time")
|
||||
appointment.status = "rejected"
|
||||
appointment.status = "rejected_by_sto"
|
||||
appointment.service_center_comment = payload.comment
|
||||
await notify_service_staff(
|
||||
session,
|
||||
@@ -365,13 +369,13 @@ async def get_sto_dashboard(
|
||||
confirmed_appointments = int(
|
||||
(await session.execute(select(func.count(ServiceAppointment.id)).where(
|
||||
ServiceAppointment.service_center_id == service_center_id,
|
||||
ServiceAppointment.status == "confirmed",
|
||||
ServiceAppointment.status.in_(["confirmed", "confirmed_by_sto"]),
|
||||
))).scalar_one() or 0
|
||||
)
|
||||
active_work_orders = int(
|
||||
(await session.execute(select(func.count(ServiceVisit.id)).where(
|
||||
ServiceVisit.service_center_id == service_center_id,
|
||||
ServiceVisit.status.in_(["draft", "pending_owner_confirmation"]),
|
||||
ServiceVisit.status.in_(["draft", "diagnosis", "waiting_owner_approval", "approved_by_owner", "in_progress", "pending_owner_confirmation"]),
|
||||
))).scalar_one() or 0
|
||||
)
|
||||
completed_result = await session.execute(
|
||||
@@ -465,7 +469,7 @@ async def confirm_appointment(
|
||||
duration_minutes=appointment.estimated_duration_minutes,
|
||||
exclude_appointment_id=appointment.id,
|
||||
)
|
||||
appointment.status = "confirmed"
|
||||
appointment.status = "confirmed_by_sto"
|
||||
appointment.confirmed_start_at = appointment.requested_start_at
|
||||
appointment.confirmed_end_at = appointment.requested_end_at
|
||||
appointment.service_center_comment = payload.comment
|
||||
@@ -492,9 +496,9 @@ async def reject_appointment(
|
||||
current_user: User = Depends(get_current_telegram_user),
|
||||
) -> ServiceAppointment:
|
||||
appointment = await _appointment_for_sto(session, appointment_id, current_user)
|
||||
if appointment.status in {"completed", "cancelled_by_customer", "cancelled_by_sto"}:
|
||||
if appointment.status in {"completed", "cancelled_by_owner", "cancelled_by_customer", "cancelled_by_sto"}:
|
||||
raise HTTPException(status_code=409, detail="Appointment cannot be rejected")
|
||||
appointment.status = "rejected"
|
||||
appointment.status = "rejected_by_sto"
|
||||
appointment.service_center_comment = payload.comment
|
||||
await create_service_notification(
|
||||
session,
|
||||
@@ -519,7 +523,7 @@ async def propose_appointment_time(
|
||||
current_user: User = Depends(get_current_telegram_user),
|
||||
) -> ServiceAppointment:
|
||||
appointment = await _appointment_for_sto(session, appointment_id, current_user)
|
||||
if appointment.status in {"completed", "cancelled_by_customer", "cancelled_by_sto"}:
|
||||
if appointment.status in {"completed", "cancelled_by_owner", "cancelled_by_customer", "cancelled_by_sto"}:
|
||||
raise HTTPException(status_code=409, detail="Appointment cannot be changed")
|
||||
duration = estimate_duration(appointment.service_type, payload.estimated_duration_minutes or appointment.estimated_duration_minutes)
|
||||
proposed_start = _utc(payload.proposed_start_at)
|
||||
@@ -559,7 +563,7 @@ async def create_work_order_from_appointment(
|
||||
) -> ServiceVisit:
|
||||
appointment = await _appointment_for_sto(session, appointment_id, current_user)
|
||||
employee = await ensure_service_employee(session, appointment.service_center_id, current_user, {"owner", "manager", "receptionist"})
|
||||
if appointment.status != "confirmed":
|
||||
if appointment.status not in {"confirmed", "confirmed_by_sto"}:
|
||||
raise HTTPException(status_code=409, detail="Only confirmed appointment can become work order")
|
||||
if appointment.linked_work_order_id:
|
||||
visit = await session.get(ServiceVisit, appointment.linked_work_order_id)
|
||||
@@ -569,15 +573,32 @@ async def create_work_order_from_appointment(
|
||||
visit = ServiceVisit(
|
||||
service_center_id=appointment.service_center_id,
|
||||
vehicle_id=appointment.vehicle_id,
|
||||
owner_id=appointment.owner_id,
|
||||
created_by_employee_id=employee.id,
|
||||
assigned_employee_id=employee.id,
|
||||
visit_date=(appointment.confirmed_start_at or appointment.requested_start_at).date(),
|
||||
odometer=payload.odometer,
|
||||
status="draft",
|
||||
customer_complaint=appointment.customer_comment,
|
||||
notes=payload.notes or appointment.customer_comment,
|
||||
opened_at=datetime.now(UTC),
|
||||
)
|
||||
session.add(visit)
|
||||
await session.flush()
|
||||
await assign_work_order_number(session, visit)
|
||||
await add_status_history(session, visit, to_status="diagnosis", actor=current_user, comment="Created from appointment")
|
||||
appointment.linked_work_order_id = visit.id
|
||||
appointment.status = "converted_to_work_order"
|
||||
await create_service_notification(
|
||||
session,
|
||||
recipient_user_id=appointment.owner_id,
|
||||
service_center_id=appointment.service_center_id,
|
||||
appointment_id=appointment.id,
|
||||
notification_type="work_order.created",
|
||||
title="СТО создало заказ-наряд",
|
||||
body=visit.work_order_number,
|
||||
idempotency_key=f"work_order:{visit.id}:created",
|
||||
)
|
||||
await log_audit(session, actor=current_user, action="appointment.create_work_order", target_type="service_appointment", target_id=appointment_id, metadata={"service_visit_id": visit.id})
|
||||
await session.commit()
|
||||
await session.refresh(visit)
|
||||
|
||||
Reference in New Issue
Block a user