diff --git a/app/api/work_orders.py b/app/api/work_orders.py index 085b6e7..e3f140f 100644 --- a/app/api/work_orders.py +++ b/app/api/work_orders.py @@ -88,6 +88,13 @@ async def get_work_order_with_items(session: AsyncSession, work_order_id: int) - return visit +async def get_work_order_correction(session: AsyncSession, correction_id: int) -> WorkOrderCorrection: + correction = await session.get(WorkOrderCorrection, correction_id) + if correction is None: + raise HTTPException(status_code=404, detail="Work order correction not found") + return correction + + async def ensure_work_order_sto_access( session: AsyncSession, visit: ServiceVisit, user: User, allowed_roles: set[str] | None = None ) -> None: @@ -539,6 +546,26 @@ async def request_vehicle_profile_details( return visit +@router.get("/{work_order_id}/corrections", response_model=list[WorkOrderCorrectionRead]) +async def list_work_order_corrections( + work_order_id: int, + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_current_telegram_user), +) -> list[WorkOrderCorrection]: + 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(WorkOrderCorrection) + .where(WorkOrderCorrection.service_visit_id == visit.id) + .order_by(WorkOrderCorrection.created_at.desc(), WorkOrderCorrection.id.desc()) + ) + return list(result.scalars()) + + @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, @@ -559,6 +586,19 @@ async def create_work_order_correction( created_version=visit.version or 1, ) session.add(correction) + vehicle = await session.get(Car, visit.vehicle_id) + if payload.owner_approval_required and vehicle is not None: + await create_service_notification( + session, + recipient_user_id=vehicle.owner_id, + service_center_id=visit.service_center_id, + notification_type="work_order.correction_waiting_owner_approval", + title="СТО просит согласовать правку заказ-наряда", + body=payload.reason, + idempotency_key=f"work_order:{visit.id}:correction:{visit.version or 1}:{payload.reason[:80]}", + web_app_url=work_order_webapp_url(visit.id), + button_text="Открыть заказ-наряд", + ) await log_audit( session, actor=current_user, @@ -572,6 +612,60 @@ async def create_work_order_correction( return correction +@router.post("/corrections/{correction_id}/approve", response_model=WorkOrderCorrectionRead) +async def approve_work_order_correction( + correction_id: int, + payload: WorkOrderDecision, + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_current_telegram_user), +) -> WorkOrderCorrection: + correction = await get_work_order_correction(session, correction_id) + visit = await get_work_order(session, correction.service_visit_id) + await ensure_work_order_owner_access(session, visit, current_user) + if correction.status != "pending": + raise HTTPException(status_code=409, detail="Correction is already resolved") + correction.status = "approved" + correction.resolved_at = datetime.now(UTC) + await log_audit( + session, + actor=current_user, + action="work_order.correction.approve", + target_type="work_order_correction", + target_id=correction.id, + metadata={"comment": payload.comment}, + ) + await session.commit() + await session.refresh(correction) + return correction + + +@router.post("/corrections/{correction_id}/reject", response_model=WorkOrderCorrectionRead) +async def reject_work_order_correction( + correction_id: int, + payload: WorkOrderDecision, + session: AsyncSession = Depends(get_session), + current_user: User = Depends(get_current_telegram_user), +) -> WorkOrderCorrection: + correction = await get_work_order_correction(session, correction_id) + visit = await get_work_order(session, correction.service_visit_id) + await ensure_work_order_owner_access(session, visit, current_user) + if correction.status != "pending": + raise HTTPException(status_code=409, detail="Correction is already resolved") + correction.status = "rejected" + correction.resolved_at = datetime.now(UTC) + await log_audit( + session, + actor=current_user, + action="work_order.correction.reject", + target_type="work_order_correction", + target_id=correction.id, + metadata={"comment": payload.comment}, + ) + 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, diff --git a/tests/test_production_flows.py b/tests/test_production_flows.py index dff040d..dfba02b 100644 --- a/tests/test_production_flows.py +++ b/tests/test_production_flows.py @@ -50,6 +50,19 @@ async def test_employee_invite_activation_revoked_and_expired( dashboard = await client.get(f"/api/sto/dashboard?service_center_id={center['id']}", headers=other_auth_headers) assert dashboard.status_code == 200 + other_center = await create_verified_center( + client, + auth_headers, + admin_auth_headers, + internal_headers, + "Other Tenant Service", + ) + cross_tenant_dashboard = await client.get( + f"/api/sto/dashboard?service_center_id={other_center['id']}", + headers=other_auth_headers, + ) + assert cross_tenant_dashboard.status_code == 403 + revoked_invite = ( await client.post( f"/api/service-centers/{center['id']}/employees/invite", @@ -193,6 +206,22 @@ async def test_work_order_completion_creates_vehicle_records_and_updates_costs( ) assert correction.status_code == 201 assert correction.json()["created_version"] == completed.json()["version"] + corrections = await client.get(f"/api/work-orders/{work_order['id']}/corrections", headers=other_auth_headers) + assert corrections.status_code == 200 + assert corrections.json()[0]["id"] == correction.json()["id"] + approved_correction = await client.post( + f"/api/work-orders/corrections/{correction.json()['id']}/approve", + headers=other_auth_headers, + json={"comment": "Correction accepted"}, + ) + assert approved_correction.status_code == 200 + assert approved_correction.json()["status"] == "approved" + repeated_correction_decision = await client.post( + f"/api/work-orders/corrections/{correction.json()['id']}/reject", + headers=other_auth_headers, + json={"comment": "Too late"}, + ) + assert repeated_correction_decision.status_code == 409 service_history = await client.get( f"/api/my/vehicles/{vehicle['id']}/service-history",