Law/app/api/admin/requests_modules/service.py
2026-03-17 10:30:43 +03:00

753 lines
32 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import logging
from time import perf_counter
from datetime import datetime, timezone
from typing import Any
from uuid import UUID, uuid4
from fastapi import HTTPException
from sqlalchemy import case, func, or_, update
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
from sqlalchemy.orm import Session
from app.models.admin_user import AdminUser
from app.models.attachment import Attachment
from app.models.audit_log import AuditLog
from app.models.invoice import Invoice
from app.models.notification import Notification
from app.models.request import Request
from app.models.request_service_request import RequestServiceRequest
from app.schemas.admin import RequestAdminCreate, RequestAdminPatch
from app.services.chat_secure_service import (
get_chat_activity_summary,
list_messages_for_request_window,
mark_messages_read_for_staff,
serialize_messages_for_request,
)
from app.schemas.universal import UniversalQuery
from app.services.billing_flow import apply_billing_transition_effects
from app.services.notifications import (
EVENT_STATUS as NOTIFICATION_EVENT_STATUS,
mark_admin_notifications_read,
notify_request_event,
)
from app.services.request_assignment_events import apply_assignment_change
from app.services.request_read_markers import (
EVENT_STATUS,
clear_unread_for_lawyer,
mark_unread_for_client,
)
from app.services.request_deadline import initial_important_date_at
from app.services.request_status import apply_status_change_effects
from app.services.request_templates import validate_required_topic_fields_or_400
from app.services.status_flow import transition_allowed_for_topic
from app.services.status_transition_requirements import validate_transition_requirements_or_400
from app.services.universal_query import apply_universal_query
from .common import normalize_important_date_or_default
from .permissions import (
REQUEST_FINANCIAL_FIELDS,
active_lawyer_or_400,
client_for_request_payload_or_400,
ensure_lawyer_can_manage_request_or_403,
ensure_lawyer_can_view_request_or_403,
request_uuid_or_400,
)
from .status_flow import apply_request_special_filters, get_request_status_route_service, split_request_special_filters
_WORKSPACE_LOG = logging.getLogger("uvicorn.error")
INITIAL_WORKSPACE_CHAT_WINDOW_LIMIT = 20
def query_requests_service(uq: UniversalQuery, db: Session, admin: dict) -> dict[str, Any]:
base_query = db.query(Request)
role = str(admin.get("role") or "").upper()
actor = str(admin.get("sub") or "").strip()
if role == "LAWYER":
if not actor:
raise HTTPException(status_code=401, detail="Некорректный токен")
base_query = base_query.filter(
or_(
Request.assigned_lawyer_id == actor,
Request.assigned_lawyer_id.is_(None),
)
)
regular_uq, special_filters = split_request_special_filters(uq)
base_query = apply_request_special_filters(
base_query,
db=db,
role=role,
actor_id=actor,
special_filters=special_filters,
)
q = apply_universal_query(base_query, Request, regular_uq)
total = q.count()
rows = q.offset(uq.page.offset).limit(uq.page.limit).all()
row_ids = [str(row.id) for row in rows if row and row.id]
unread_service_requests_by_request: dict[str, int] = {}
viewer_unread_by_request: dict[str, dict[str, Any]] = {}
if row_ids:
unread_query = (
db.query(RequestServiceRequest.request_id, func.count(RequestServiceRequest.id))
.filter(RequestServiceRequest.request_id.in_(row_ids))
)
if role == "LAWYER":
unread_query = unread_query.filter(
RequestServiceRequest.type == "CURATOR_CONTACT",
RequestServiceRequest.assigned_lawyer_id == actor,
RequestServiceRequest.lawyer_unread.is_(True),
)
else:
unread_query = unread_query.filter(RequestServiceRequest.admin_unread.is_(True))
unread_rows = unread_query.group_by(RequestServiceRequest.request_id).all()
unread_service_requests_by_request = {str(request_id): int(count or 0) for request_id, count in unread_rows if request_id}
if actor:
try:
actor_uuid = UUID(str(actor))
except ValueError:
actor_uuid = None
if actor_uuid is not None:
try:
notif_rows = (
db.query(Notification.request_id, Notification.event_type, func.count(Notification.id))
.filter(
Notification.recipient_type == "ADMIN_USER",
Notification.recipient_admin_user_id == actor_uuid,
Notification.is_read.is_(False),
Notification.request_id.in_(row_ids),
)
.group_by(Notification.request_id, Notification.event_type)
.all()
)
except SQLAlchemyError:
notif_rows = []
for request_id, event_type, count in notif_rows:
request_key = str(request_id or "")
if not request_key:
continue
bucket = viewer_unread_by_request.setdefault(request_key, {"total": 0, "by_event": {}})
event_key = str(event_type or "").strip().upper()
event_count = int(count or 0)
if event_key:
bucket["by_event"][event_key] = int(bucket["by_event"].get(event_key, 0)) + event_count
bucket["total"] = int(bucket["total"]) + event_count
return {
"rows": [
{
"id": str(r.id),
"track_number": r.track_number,
"client_id": str(r.client_id) if r.client_id else None,
"status_code": r.status_code,
"client_name": r.client_name,
"client_phone": r.client_phone,
"topic_code": r.topic_code,
"important_date_at": r.important_date_at.isoformat() if r.important_date_at else None,
"effective_rate": float(r.effective_rate) if r.effective_rate is not None else None,
"request_cost": float(r.request_cost) if r.request_cost is not None else None,
"invoice_amount": float(r.invoice_amount) if r.invoice_amount is not None else None,
"paid_at": r.paid_at.isoformat() if r.paid_at else None,
"paid_by_admin_id": r.paid_by_admin_id,
"client_has_unread_updates": r.client_has_unread_updates,
"client_unread_event_type": r.client_unread_event_type,
"lawyer_has_unread_updates": r.lawyer_has_unread_updates,
"lawyer_unread_event_type": r.lawyer_unread_event_type,
"service_requests_unread_count": int(unread_service_requests_by_request.get(str(r.id), 0)),
"has_service_requests_unread": bool(unread_service_requests_by_request.get(str(r.id), 0)),
"viewer_unread_total": int((viewer_unread_by_request.get(str(r.id)) or {}).get("total", 0)),
"viewer_unread_by_event": dict((viewer_unread_by_request.get(str(r.id)) or {}).get("by_event", {})),
"created_at": r.created_at.isoformat() if r.created_at else None,
"updated_at": r.updated_at.isoformat() if r.updated_at else None,
}
for r in rows
],
"total": total,
}
def create_request_service(payload: RequestAdminCreate, db: Session, admin: dict) -> dict[str, Any]:
actor_role = str(admin.get("role") or "").upper()
if actor_role == "LAWYER" and str(payload.assigned_lawyer_id or "").strip():
raise HTTPException(status_code=403, detail="Юрист не может назначать заявку при создании")
if actor_role == "LAWYER":
forbidden_fields = sorted(REQUEST_FINANCIAL_FIELDS.intersection(set(payload.model_fields_set)))
if forbidden_fields:
raise HTTPException(status_code=403, detail="Юрист не может изменять финансовые поля заявки")
validate_required_topic_fields_or_400(db, payload.topic_code, payload.extra_fields)
track = payload.track_number or f"TRK-{uuid4().hex[:10].upper()}"
responsible = str(admin.get("email") or "").strip() or "Администратор системы"
client = client_for_request_payload_or_400(
db,
client_id=payload.client_id,
client_name=payload.client_name,
client_phone=payload.client_phone,
responsible=responsible,
)
assigned_lawyer_id = str(payload.assigned_lawyer_id or "").strip() or None
effective_rate = payload.effective_rate
if assigned_lawyer_id:
assigned_lawyer = active_lawyer_or_400(db, assigned_lawyer_id)
assigned_lawyer_id = str(assigned_lawyer.id)
if effective_rate is None:
effective_rate = assigned_lawyer.default_rate
important_date_at = payload.important_date_at or initial_important_date_at()
row = Request(
track_number=track,
client_id=client.id,
client_name=client.full_name,
client_phone=client.phone,
topic_code=payload.topic_code,
status_code=payload.status_code,
important_date_at=important_date_at,
description=payload.description,
extra_fields=payload.extra_fields,
assigned_lawyer_id=assigned_lawyer_id,
effective_rate=effective_rate,
request_cost=payload.request_cost,
invoice_amount=payload.invoice_amount,
paid_at=payload.paid_at,
paid_by_admin_id=payload.paid_by_admin_id,
total_attachments_bytes=payload.total_attachments_bytes,
responsible=responsible,
)
try:
db.add(row)
db.commit()
db.refresh(row)
except IntegrityError as exc:
db.rollback()
raise HTTPException(status_code=400, detail="Заявка с таким номером уже существует") from exc
if assigned_lawyer_id:
apply_assignment_change(
db,
request=row,
old_lawyer_id=None,
new_lawyer_id=assigned_lawyer_id,
actor_role=str(admin.get("role") or "").upper() or "ADMIN",
actor_admin_user_id=admin.get("sub"),
responsible=responsible,
actor_name=str(admin.get("email") or "").strip() or "Администратор системы",
)
db.add(row)
db.commit()
db.refresh(row)
return {"id": str(row.id), "track_number": row.track_number}
def update_request_service(request_id: str, payload: RequestAdminPatch, db: Session, admin: dict) -> dict[str, Any]:
request_uuid = request_uuid_or_400(request_id)
row = db.get(Request, request_uuid)
if not row:
raise HTTPException(status_code=404, detail="Заявка не найдена")
ensure_lawyer_can_manage_request_or_403(admin, row)
changes = payload.model_dump(exclude_unset=True)
actor_role = str(admin.get("role") or "").upper()
if actor_role == "LAWYER":
if "assigned_lawyer_id" in changes:
raise HTTPException(status_code=403, detail='Назначение доступно только через действие "Взять в работу"')
forbidden_fields = sorted(REQUEST_FINANCIAL_FIELDS.intersection(set(changes.keys())))
if forbidden_fields:
raise HTTPException(status_code=403, detail="Юрист не может изменять финансовые поля заявки")
if actor_role == "ADMIN" and "assigned_lawyer_id" in changes:
assigned_raw = changes.get("assigned_lawyer_id")
if assigned_raw is None or not str(assigned_raw).strip():
changes["assigned_lawyer_id"] = None
else:
assigned_lawyer = active_lawyer_or_400(db, str(assigned_raw))
changes["assigned_lawyer_id"] = str(assigned_lawyer.id)
if row.effective_rate is None and "effective_rate" not in changes:
changes["effective_rate"] = assigned_lawyer.default_rate
old_status = str(row.status_code or "")
old_assigned_lawyer_id = str(row.assigned_lawyer_id or "").strip()
responsible = str(admin.get("email") or "").strip() or "Администратор системы"
if {"client_id", "client_name", "client_phone"}.intersection(set(changes.keys())):
client = client_for_request_payload_or_400(
db,
client_id=changes.get("client_id", row.client_id),
client_name=changes.get("client_name", row.client_name),
client_phone=changes.get("client_phone", row.client_phone),
responsible=responsible,
)
changes["client_id"] = client.id
changes["client_name"] = client.full_name
changes["client_phone"] = client.phone
status_changed = "status_code" in changes and str(changes.get("status_code") or "") != old_status
if status_changed and ("important_date_at" not in changes or changes.get("important_date_at") is None):
changes["important_date_at"] = normalize_important_date_or_default(None)
if status_changed:
next_status = str(changes.get("status_code") or "").strip()
if not transition_allowed_for_topic(
db,
str(row.topic_code or "").strip() or None,
old_status,
next_status,
):
raise HTTPException(status_code=400, detail="Переход статуса не разрешен для выбранной темы")
extra_fields_override = changes.get("extra_fields")
validate_transition_requirements_or_400(
db,
row,
old_status,
next_status,
extra_fields_override=extra_fields_override if isinstance(extra_fields_override, dict) else None,
)
for key, value in changes.items():
setattr(row, key, value)
new_assigned_lawyer_id = str(row.assigned_lawyer_id or "").strip()
assigned_changed = old_assigned_lawyer_id != new_assigned_lawyer_id
if status_changed:
next_status = str(changes.get("status_code") or "")
important_date_at = row.important_date_at
billing_note = apply_billing_transition_effects(
db,
req=row,
from_status=old_status,
to_status=next_status,
admin=admin,
responsible=responsible,
)
mark_unread_for_client(row, EVENT_STATUS)
apply_status_change_effects(
db,
row,
from_status=old_status,
to_status=next_status,
admin=admin,
responsible=responsible,
)
notify_request_event(
db,
request=row,
event_type=NOTIFICATION_EVENT_STATUS,
actor_role=str(admin.get("role") or "").upper() or "ADMIN",
actor_admin_user_id=admin.get("sub"),
body=(
f"{old_status} -> {next_status}"
+ (f"\nВажная дата: {important_date_at.isoformat()}" if important_date_at else "")
+ (f"\n{billing_note}" if billing_note else "")
),
responsible=responsible,
)
if actor_role == "ADMIN" and assigned_changed and new_assigned_lawyer_id:
apply_assignment_change(
db,
request=row,
old_lawyer_id=old_assigned_lawyer_id,
new_lawyer_id=new_assigned_lawyer_id,
actor_role="ADMIN",
actor_admin_user_id=admin.get("sub"),
responsible=responsible,
actor_name=str(admin.get("email") or "").strip() or "Администратор системы",
)
try:
db.add(row)
db.commit()
db.refresh(row)
except IntegrityError as exc:
db.rollback()
raise HTTPException(status_code=400, detail="Заявка с таким номером уже существует") from exc
return {"status": "обновлено", "id": str(row.id), "track_number": row.track_number}
def delete_request_service(request_id: str, db: Session, admin: dict) -> dict[str, Any]:
request_uuid = request_uuid_or_400(request_id)
row = db.get(Request, request_uuid)
if not row:
raise HTTPException(status_code=404, detail="Заявка не найдена")
ensure_lawyer_can_manage_request_or_403(admin, row)
db.delete(row)
db.commit()
return {"status": "удалено"}
def get_request_service(request_id: str, db: Session, admin: dict) -> dict[str, Any]:
request_uuid = request_uuid_or_400(request_id)
req = db.get(Request, request_uuid)
if not req:
raise HTTPException(status_code=404, detail="Заявка не найдена")
ensure_lawyer_can_view_request_or_403(admin, req)
_apply_request_open_side_effects(db, req, admin, mark_chat_read=False)
return _serialize_request_row(req)
def _serialize_request_row(req: Request) -> dict[str, Any]:
return {
"id": str(req.id),
"track_number": req.track_number,
"client_id": str(req.client_id) if req.client_id else None,
"client_name": req.client_name,
"client_phone": req.client_phone,
"topic_code": req.topic_code,
"status_code": req.status_code,
"important_date_at": req.important_date_at.isoformat() if req.important_date_at else None,
"description": req.description,
"extra_fields": req.extra_fields,
"assigned_lawyer_id": req.assigned_lawyer_id,
"effective_rate": float(req.effective_rate) if req.effective_rate is not None else None,
"request_cost": float(req.request_cost) if req.request_cost is not None else None,
"invoice_amount": float(req.invoice_amount) if req.invoice_amount is not None else None,
"paid_at": req.paid_at.isoformat() if req.paid_at else None,
"paid_by_admin_id": req.paid_by_admin_id,
"total_attachments_bytes": req.total_attachments_bytes,
"client_has_unread_updates": req.client_has_unread_updates,
"client_unread_event_type": req.client_unread_event_type,
"lawyer_has_unread_updates": req.lawyer_has_unread_updates,
"lawyer_unread_event_type": req.lawyer_unread_event_type,
"created_at": req.created_at.isoformat() if req.created_at else None,
"updated_at": req.updated_at.isoformat() if req.updated_at else None,
}
def _apply_request_open_side_effects(db: Session, req: Request, admin: dict, *, mark_chat_read: bool) -> None:
changed = False
if str(admin.get("role") or "").upper() == "LAWYER" and clear_unread_for_lawyer(req):
changed = True
db.add(req)
read_count = mark_admin_notifications_read(
db,
admin_user_id=admin.get("sub"),
request_id=req.id,
responsible=str(admin.get("email") or "").strip() or "Администратор системы",
)
if read_count:
changed = True
if mark_chat_read and mark_messages_read_for_staff(db, request_id=req.id, commit=False):
changed = True
if changed:
db.commit()
if str(admin.get("role") or "").upper() == "LAWYER":
db.refresh(req)
def _serialize_request_attachment(row: Attachment) -> dict[str, Any]:
return {
"id": str(row.id),
"request_id": str(row.request_id),
"message_id": str(row.message_id) if row.message_id else None,
"file_name": row.file_name,
"mime_type": row.mime_type,
"size_bytes": int(row.size_bytes or 0),
"s3_key": row.s3_key,
"immutable": bool(row.immutable),
"scan_status": row.scan_status,
"scan_signature": row.scan_signature,
"scan_error": row.scan_error,
"scanned_at": row.scanned_at.isoformat() if row.scanned_at else None,
"created_at": row.created_at.isoformat() if row.created_at else None,
"updated_at": row.updated_at.isoformat() if row.updated_at else None,
"responsible": row.responsible,
}
def _serialize_request_invoice(row: Invoice) -> dict[str, Any]:
return {
"id": str(row.id),
"invoice_number": row.invoice_number,
"request_id": str(row.request_id),
"client_id": str(row.client_id) if row.client_id else None,
"status": row.status,
"amount": float(row.amount) if row.amount is not None else None,
"currency": row.currency,
"payer_display_name": row.payer_display_name,
"issued_by_admin_user_id": str(row.issued_by_admin_user_id) if row.issued_by_admin_user_id else None,
"issued_by_role": row.issued_by_role,
"issued_at": row.issued_at.isoformat() if row.issued_at else None,
"paid_at": row.paid_at.isoformat() if row.paid_at else None,
"created_at": row.created_at.isoformat() if row.created_at else None,
"updated_at": row.updated_at.isoformat() if row.updated_at else None,
"responsible": row.responsible,
"pdf_url": f"/api/admin/invoices/{row.id}/pdf",
}
def get_request_workspace_service(request_id: str, db: Session, admin: dict, *, include_related: bool = True) -> dict[str, Any]:
started_at = perf_counter()
request_uuid = request_uuid_or_400(request_id)
req = db.get(Request, request_uuid)
if req is None:
raise HTTPException(status_code=404, detail="Заявка не найдена")
ensure_lawyer_can_view_request_or_403(admin, req)
side_effects_started_at = perf_counter()
_apply_request_open_side_effects(db, req, admin, mark_chat_read=True)
side_effects_ms = (perf_counter() - side_effects_started_at) * 1000.0
serialize_request_started_at = perf_counter()
request_payload = _serialize_request_row(req)
request_row_ms = (perf_counter() - serialize_request_started_at) * 1000.0
messages_started_at = perf_counter()
message_rows, messages_has_more = list_messages_for_request_window(
db,
req.id,
limit=INITIAL_WORKSPACE_CHAT_WINDOW_LIMIT,
before_count=0,
)
messages_query_ms = (perf_counter() - messages_started_at) * 1000.0
serialize_messages_started_at = perf_counter()
serialized_messages = serialize_messages_for_request(
db,
req.id,
message_rows,
request_extra_fields=req.extra_fields,
include_bodies=False,
)
serialize_messages_ms = (perf_counter() - serialize_messages_started_at) * 1000.0
messages_total = int(get_chat_activity_summary(db, req.id).get("message_count") or len(message_rows))
messages_loaded_count = len(message_rows)
attachment_rows: list[Attachment] = []
invoice_rows: list[Invoice] = []
status_route_payload: dict[str, Any] = {
"nodes": [],
"history": [],
"available_statuses": [],
"current_important_date_at": request_payload.get("important_date_at"),
}
attachments_query_ms = 0.0
invoices_query_ms = 0.0
status_route_ms = 0.0
if include_related:
attachments_started_at = perf_counter()
attachment_rows = (
db.query(Attachment)
.filter(Attachment.request_id == req.id)
.order_by(Attachment.created_at.asc(), Attachment.id.asc())
.all()
)
attachments_query_ms = (perf_counter() - attachments_started_at) * 1000.0
role = str(admin.get("role") or "").upper()
if role in {"ADMIN", "LAWYER"}:
invoices_started_at = perf_counter()
invoice_rows = (
db.query(Invoice)
.filter(Invoice.request_id == req.id)
.order_by(Invoice.issued_at.desc(), Invoice.id.desc())
.all()
)
invoices_query_ms = (perf_counter() - invoices_started_at) * 1000.0
status_route_started_at = perf_counter()
status_route_payload = get_request_status_route_service(request_id, db, admin, request_row=req)
status_route_ms = (perf_counter() - status_route_started_at) * 1000.0
paid_invoices = [row for row in invoice_rows if str(row.status or "").upper() == "PAID"]
paid_total = round(sum(float(row.amount or 0) for row in paid_invoices), 2)
latest_paid_at = None
for row in paid_invoices:
if row.paid_at is None:
continue
if latest_paid_at is None or row.paid_at > latest_paid_at:
latest_paid_at = row.paid_at
payload = {
"request": request_payload,
"messages": serialized_messages,
"messages_total": messages_total,
"messages_has_more": messages_has_more,
"messages_loaded_count": messages_loaded_count,
"attachments": [_serialize_request_attachment(row) for row in attachment_rows],
"invoices": [_serialize_request_invoice(row) for row in invoice_rows],
"finance_summary": {
"request_cost": request_payload.get("request_cost"),
"effective_rate": request_payload.get("effective_rate"),
"paid_total": paid_total,
"last_paid_at": latest_paid_at.isoformat() if latest_paid_at else request_payload.get("paid_at"),
},
"status_route": status_route_payload,
}
total_ms = (perf_counter() - started_at) * 1000.0
_WORKSPACE_LOG.info(
"workspace request_id=%s include_related=%s total_ms=%.2f side_effects_ms=%.2f request_row_ms=%.2f "
"messages_query_ms=%.2f serialize_messages_ms=%.2f attachments_query_ms=%.2f invoices_query_ms=%.2f "
"status_route_ms=%.2f messages=%s attachments=%s invoices=%s messages_total=%s",
str(req.id),
bool(include_related),
total_ms,
side_effects_ms,
request_row_ms,
messages_query_ms,
serialize_messages_ms,
attachments_query_ms,
invoices_query_ms,
status_route_ms,
len(message_rows),
len(attachment_rows),
len(invoice_rows),
messages_total,
)
return payload
def claim_request_service(request_id: str, db: Session, admin: dict) -> dict[str, Any]:
request_uuid = request_uuid_or_400(request_id)
lawyer_sub = str(admin.get("sub") or "").strip()
if not lawyer_sub:
raise HTTPException(status_code=401, detail="Некорректный токен")
try:
lawyer_uuid = UUID(lawyer_sub)
except ValueError as exc:
raise HTTPException(status_code=401, detail="Некорректный токен") from exc
lawyer = db.get(AdminUser, lawyer_uuid)
if not lawyer or str(lawyer.role or "").upper() != "LAWYER" or not bool(lawyer.is_active):
raise HTTPException(status_code=403, detail="Доступно только активному юристу")
now = datetime.now(timezone.utc)
responsible = str(admin.get("email") or "").strip() or "Администратор системы"
stmt = (
update(Request)
.where(Request.id == request_uuid, Request.assigned_lawyer_id.is_(None))
.values(
assigned_lawyer_id=str(lawyer_uuid),
effective_rate=case((Request.effective_rate.is_(None), lawyer.default_rate), else_=Request.effective_rate),
updated_at=now,
responsible=responsible,
)
)
try:
updated_rows = db.execute(stmt).rowcount or 0
if updated_rows == 0:
existing = db.get(Request, request_uuid)
if existing is None:
db.rollback()
raise HTTPException(status_code=404, detail="Заявка не найдена")
db.rollback()
raise HTTPException(status_code=409, detail="Заявка уже назначена")
db.add(
AuditLog(
actor_admin_id=lawyer_uuid,
entity="requests",
entity_id=str(request_uuid),
action="MANUAL_CLAIM",
diff={"assigned_lawyer_id": str(lawyer_uuid)},
)
)
db.commit()
except HTTPException:
raise
except Exception:
db.rollback()
raise
row = db.get(Request, request_uuid)
if row is None:
raise HTTPException(status_code=404, detail="Заявка не найдена")
apply_assignment_change(
db,
request=row,
old_lawyer_id=None,
new_lawyer_id=str(lawyer_uuid),
actor_role="LAWYER",
actor_admin_user_id=str(lawyer_uuid),
responsible=responsible,
actor_name=str(lawyer.name or lawyer.email or lawyer_uuid),
)
db.add(row)
db.commit()
db.refresh(row)
return {
"status": "claimed",
"id": str(row.id),
"track_number": row.track_number,
"assigned_lawyer_id": row.assigned_lawyer_id,
}
def reassign_request_service(request_id: str, lawyer_id: str, db: Session, admin: dict) -> dict[str, Any]:
request_uuid = request_uuid_or_400(request_id)
try:
lawyer_uuid = UUID(str(lawyer_id))
except ValueError as exc:
raise HTTPException(status_code=400, detail="Некорректный идентификатор юриста") from exc
target_lawyer = db.get(AdminUser, lawyer_uuid)
if not target_lawyer or str(target_lawyer.role or "").upper() != "LAWYER" or not bool(target_lawyer.is_active):
raise HTTPException(status_code=400, detail="Можно переназначить только на активного юриста")
req = db.get(Request, request_uuid)
if req is None:
raise HTTPException(status_code=404, detail="Заявка не найдена")
if req.assigned_lawyer_id is None:
raise HTTPException(status_code=400, detail="Заявка не назначена")
if str(req.assigned_lawyer_id) == str(lawyer_uuid):
raise HTTPException(status_code=400, detail="Заявка уже назначена на выбранного юриста")
old_assigned = str(req.assigned_lawyer_id)
now = datetime.now(timezone.utc)
responsible = str(admin.get("email") or "").strip() or "Администратор системы"
admin_actor_id = None
try:
admin_actor_id = UUID(str(admin.get("sub") or ""))
except ValueError:
admin_actor_id = None
stmt = (
update(Request)
.where(Request.id == request_uuid, Request.assigned_lawyer_id == old_assigned)
.values(
assigned_lawyer_id=str(lawyer_uuid),
effective_rate=case((Request.effective_rate.is_(None), target_lawyer.default_rate), else_=Request.effective_rate),
updated_at=now,
responsible=responsible,
)
)
try:
updated_rows = db.execute(stmt).rowcount or 0
if updated_rows == 0:
db.rollback()
raise HTTPException(status_code=409, detail="Заявка уже была переназначена")
db.add(
AuditLog(
actor_admin_id=admin_actor_id,
entity="requests",
entity_id=str(request_uuid),
action="MANUAL_REASSIGN",
diff={"from_lawyer_id": old_assigned, "to_lawyer_id": str(lawyer_uuid)},
)
)
db.commit()
except HTTPException:
raise
except Exception:
db.rollback()
raise
row = db.get(Request, request_uuid)
if row is None:
raise HTTPException(status_code=404, detail="Заявка не найдена")
apply_assignment_change(
db,
request=row,
old_lawyer_id=old_assigned,
new_lawyer_id=str(lawyer_uuid),
actor_role="ADMIN",
actor_admin_user_id=admin.get("sub"),
responsible=responsible,
actor_name=str(admin.get("email") or "").strip() or "Администратор системы",
)
db.add(row)
db.commit()
db.refresh(row)
return {
"status": "reassigned",
"id": str(row.id),
"track_number": row.track_number,
"from_lawyer_id": old_assigned,
"assigned_lawyer_id": row.assigned_lawyer_id,
}