work-order-hardening
This commit is contained in:
@@ -88,6 +88,13 @@ async def get_work_order_with_items(session: AsyncSession, work_order_id: int) -
|
|||||||
return visit
|
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(
|
async def ensure_work_order_sto_access(
|
||||||
session: AsyncSession, visit: ServiceVisit, user: User, allowed_roles: set[str] | None = None
|
session: AsyncSession, visit: ServiceVisit, user: User, allowed_roles: set[str] | None = None
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -539,6 +546,26 @@ async def request_vehicle_profile_details(
|
|||||||
return visit
|
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)
|
@router.post("/{work_order_id}/corrections", response_model=WorkOrderCorrectionRead, status_code=status.HTTP_201_CREATED)
|
||||||
async def create_work_order_correction(
|
async def create_work_order_correction(
|
||||||
work_order_id: int,
|
work_order_id: int,
|
||||||
@@ -559,6 +586,19 @@ async def create_work_order_correction(
|
|||||||
created_version=visit.version or 1,
|
created_version=visit.version or 1,
|
||||||
)
|
)
|
||||||
session.add(correction)
|
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(
|
await log_audit(
|
||||||
session,
|
session,
|
||||||
actor=current_user,
|
actor=current_user,
|
||||||
@@ -572,6 +612,60 @@ async def create_work_order_correction(
|
|||||||
return 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])
|
@router.get("/{work_order_id}/status-history", response_model=list[WorkOrderStatusHistoryRead])
|
||||||
async def work_order_status_history(
|
async def work_order_status_history(
|
||||||
work_order_id: int,
|
work_order_id: int,
|
||||||
|
|||||||
@@ -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)
|
dashboard = await client.get(f"/api/sto/dashboard?service_center_id={center['id']}", headers=other_auth_headers)
|
||||||
assert dashboard.status_code == 200
|
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 = (
|
revoked_invite = (
|
||||||
await client.post(
|
await client.post(
|
||||||
f"/api/service-centers/{center['id']}/employees/invite",
|
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.status_code == 201
|
||||||
assert correction.json()["created_version"] == completed.json()["version"]
|
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(
|
service_history = await client.get(
|
||||||
f"/api/my/vehicles/{vehicle['id']}/service-history",
|
f"/api/my/vehicles/{vehicle['id']}/service-history",
|
||||||
|
|||||||
Reference in New Issue
Block a user