klg-asutk-app/backend/app/api/routes/legal_legacy.py
Yuriy a7da43be0e apply recommendations: security, get_db, exceptions, eslint, api-client
- session: set_tenant use bound param (SQL injection fix)
- health: text('SELECT 1'), REDIS_URL from config
- deps: re-export get_db from session, use settings.ENABLE_DEV_AUTH (default False)
- routes: all get_db from app.api.deps; conftest overrides deps.get_db
- main: register exception handlers from app.api.exceptions
- next.config: enable ESLint and TypeScript checks
- .eslintrc: drop @typescript-eslint/recommended; fix no-console (logger, ws-client, regulations)
- backend/.env.example added
- frontend: export apiFetch; dashboard, profile, settings, risks use api-client
- docs/ANALYSIS_AND_RECOMMENDATIONS.md

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 21:48:58 +03:00

549 lines
24 KiB
Python
Raw 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.

"""
API маршруты для системы юридических документов:
- юрисдикции, документы, перекрёстные ссылки, правовые комментарии, судебная практика
- запуск мультиагентного ИИ-анализа и подготовки документов по нормам
"""
from fastapi import APIRouter, Depends, HTTPException, status, Query
from app.api.helpers import audit, paginate_query
from sqlalchemy.orm import Session
from app.api.deps import get_current_user, require_roles
from app.api.deps import get_db
from app.models import Jurisdiction, LegalDocument, CrossReference, LegalComment, JudicialPractice
from app.schemas.legal import (
JurisdictionCreate,
JurisdictionUpdate,
JurisdictionOut,
LegalDocumentCreate,
LegalDocumentUpdate,
LegalDocumentOut,
CrossReferenceCreate,
CrossReferenceOut,
LegalCommentCreate,
LegalCommentUpdate,
LegalCommentOut,
JudicialPracticeCreate,
JudicialPracticeUpdate,
JudicialPracticeOut,
AnalysisRequest,
AnalysisResponse,
)
from app.services.legal_agents import LegalAnalysisOrchestrator
router = APIRouter(prefix="/legal", tags=["legal"])
# --- Jurisdictions ---
@router.get("/jurisdictions", response_model=list[JurisdictionOut])
def list_jurisdictions(
active_only: bool = Query(True),
db: Session = Depends(get_db),
user=Depends(get_current_user),
):
q = db.query(Jurisdiction)
if active_only:
q = q.filter(Jurisdiction.is_active.is_(True))
return q.order_by(Jurisdiction.code).all()
@router.post("/jurisdictions", response_model=JurisdictionOut, status_code=status.HTTP_201_CREATED)
def create_jurisdiction(
payload: JurisdictionCreate,
db: Session = Depends(get_db),
user=Depends(require_roles("admin")),
):
j = Jurisdiction(**payload.model_dump())
db.add(j)
db.commit()
db.refresh(j)
return j
@router.get("/jurisdictions/{jid}", response_model=JurisdictionOut)
def get_jurisdiction(jid: str, db: Session = Depends(get_db), user=Depends(get_current_user)):
j = db.get(Jurisdiction, jid)
if not j:
raise HTTPException(status_code=404, detail="Jurisdiction not found")
return j
@router.patch("/jurisdictions/{jid}", response_model=JurisdictionOut)
def update_jurisdiction(
jid: str,
payload: JurisdictionUpdate,
db: Session = Depends(get_db),
user=Depends(require_roles("admin")),
):
j = db.get(Jurisdiction, jid)
if not j:
raise HTTPException(status_code=404, detail="Jurisdiction not found")
for k, v in payload.model_dump(exclude_unset=True).items():
setattr(j, k, v)
db.commit()
db.refresh(j)
return j
# --- Legal Documents ---
@router.get("/documents", response_model=list[LegalDocumentOut])
def list_legal_documents(
page: int = Query(1, ge=1), per_page: int = Query(25, ge=1, le=100),
jurisdiction_id: str | None = Query(None),
document_type: str | None = Query(None),
limit: int = Query(100, le=500),
db: Session = Depends(get_db),
user=Depends(get_current_user),
):
q = db.query(LegalDocument)
if jurisdiction_id:
q = q.filter(LegalDocument.jurisdiction_id == jurisdiction_id)
if document_type:
q = q.filter(LegalDocument.document_type == document_type)
q = q.order_by(LegalDocument.created_at.desc())
return paginate_query(q, page, per_page)
@router.post("/documents", response_model=LegalDocumentOut, status_code=status.HTTP_201_CREATED)
def create_legal_document(
payload: LegalDocumentCreate,
db: Session = Depends(get_db),
user=Depends(require_roles("admin", "authority_inspector")),
):
d = LegalDocument(**payload.model_dump())
db.add(d)
db.commit()
db.refresh(d)
return d
@router.get("/documents/{doc_id}", response_model=LegalDocumentOut)
def get_legal_document(doc_id: str, db: Session = Depends(get_db), user=Depends(get_current_user)):
d = db.get(LegalDocument, doc_id)
if not d:
raise HTTPException(status_code=404, detail="Document not found")
return d
@router.patch("/documents/{doc_id}", response_model=LegalDocumentOut)
def update_legal_document(
doc_id: str,
payload: LegalDocumentUpdate,
db: Session = Depends(get_db),
user=Depends(require_roles("admin", "authority_inspector")),
):
d = db.get(LegalDocument, doc_id)
if not d:
raise HTTPException(status_code=404, detail="Document not found")
for k, v in payload.model_dump(exclude_unset=True).items():
setattr(d, k, v)
db.commit()
db.refresh(d)
return d
@router.get("/documents/{doc_id}/cross-references", response_model=list[CrossReferenceOut])
def list_document_cross_references(
doc_id: str,
direction: str = Query("outgoing", description="outgoing|incoming"),
db: Session = Depends(get_db),
user=Depends(get_current_user),
):
q = db.query(CrossReference)
if direction == "incoming":
q = q.filter(CrossReference.target_document_id == doc_id)
else:
q = q.filter(CrossReference.source_document_id == doc_id)
return q.limit(100).all()
# --- Cross References (ручное добавление) ---
@router.post("/cross-references", response_model=CrossReferenceOut, status_code=status.HTTP_201_CREATED)
def create_cross_reference(
payload: CrossReferenceCreate,
db: Session = Depends(get_db),
user=Depends(require_roles("admin", "authority_inspector")),
):
ref = CrossReference(**payload.model_dump())
db.add(ref)
db.commit()
db.refresh(ref)
return ref
# --- Legal Comments ---
@router.get("/comments", response_model=list[LegalCommentOut])
def list_legal_comments(
jurisdiction_id: str | None = Query(None),
document_id: str | None = Query(None),
limit: int = Query(100, le=500),
db: Session = Depends(get_db),
user=Depends(get_current_user),
):
q = db.query(LegalComment)
if jurisdiction_id:
q = q.filter(LegalComment.jurisdiction_id == jurisdiction_id)
if document_id:
q = q.filter(LegalComment.document_id == document_id)
from fastapi import Query as FQuery
return paginate_query(q.order_by(LegalComment.created_at.desc()), 1, limit)
@router.post("/comments", response_model=LegalCommentOut, status_code=status.HTTP_201_CREATED)
def create_legal_comment(
payload: LegalCommentCreate,
db: Session = Depends(get_db),
user=Depends(require_roles("admin", "authority_inspector")),
):
c = LegalComment(**payload.model_dump())
db.add(c)
db.commit()
db.refresh(c)
return c
@router.patch("/comments/{cid}", response_model=LegalCommentOut)
def update_legal_comment(
cid: str,
payload: LegalCommentUpdate,
db: Session = Depends(get_db),
user=Depends(require_roles("admin", "authority_inspector")),
):
c = db.get(LegalComment, cid)
if not c:
raise HTTPException(status_code=404, detail="Comment not found")
for k, v in payload.model_dump(exclude_unset=True).items():
setattr(c, k, v)
db.commit()
db.refresh(c)
return c
# --- Judicial Practice ---
@router.get("/judicial-practices", response_model=list[JudicialPracticeOut])
def list_judicial_practices(
jurisdiction_id: str | None = Query(None),
document_id: str | None = Query(None),
limit: int = Query(100, le=500),
db: Session = Depends(get_db),
user=Depends(get_current_user),
):
q = db.query(JudicialPractice)
if jurisdiction_id:
q = q.filter(JudicialPractice.jurisdiction_id == jurisdiction_id)
if document_id:
q = q.filter(JudicialPractice.document_id == document_id)
# nulls_last портировано под SQLite < 3.30: (col IS NULL) ASC — не-NULL первыми
return q.order_by(
JudicialPractice.decision_date.is_(None),
JudicialPractice.decision_date.desc(),
JudicialPractice.created_at.desc(),
)
return paginate_query(q, 1, limit)
@router.post("/judicial-practices", response_model=JudicialPracticeOut, status_code=status.HTTP_201_CREATED)
def create_judicial_practice(
payload: JudicialPracticeCreate,
db: Session = Depends(get_db),
user=Depends(require_roles("admin", "authority_inspector")),
):
p = JudicialPractice(**payload.model_dump())
db.add(p)
db.commit()
db.refresh(p)
return p
@router.patch("/judicial-practices/{pid}", response_model=JudicialPracticeOut)
def update_judicial_practice(
pid: str,
payload: JudicialPracticeUpdate,
db: Session = Depends(get_db),
user=Depends(require_roles("admin", "authority_inspector")),
):
p = db.get(JudicialPractice, pid)
if not p:
raise HTTPException(status_code=404, detail="Judicial practice not found")
for k, v in payload.model_dump(exclude_unset=True).items():
setattr(p, k, v)
db.commit()
db.refresh(p)
return p
# --- ИИ-анализ (мультиагентный) ---
@router.post("/analyze", response_model=AnalysisResponse)
def analyze_document(
payload: AnalysisRequest,
db: Session = Depends(get_db),
user=Depends(require_roles("admin", "authority_inspector")),
):
"""
Запуск мультиагентного анализа: классификация, соответствие нормам, перекрёстные ссылки,
подбор правовых комментариев и судебной практики, рекомендации по оформлению.
"""
orch = LegalAnalysisOrchestrator(db=db)
out = orch.run(
document_id=payload.document_id,
jurisdiction_id=payload.jurisdiction_id,
title=payload.title,
content=payload.content,
skip_agents=payload.skip_agents,
save_cross_references=payload.save_cross_references,
)
# Опционально: обновить документ в БД, если document_id передан
if payload.document_id and out.get("document_type"):
d = db.get(LegalDocument, payload.document_id)
if d:
d.document_type = out["document_type"]
d.analysis_json = out.get("analysis_json")
d.compliance_notes = out.get("compliance_notes")
db.commit()
return AnalysisResponse(
document_type=out["document_type"],
analysis_json=out.get("analysis_json"),
compliance_notes=out.get("compliance_notes"),
results=out.get("results", {}),
)
@router.post("/documents/{doc_id}/analyze", response_model=AnalysisResponse)
def analyze_existing_document(
doc_id: str,
skip_agents: list[str] | None = Query(None),
save_cross_references: bool = Query(True),
db: Session = Depends(get_db),
user=Depends(require_roles("admin", "authority_inspector")),
):
"""Запуск ИИ-анализа для уже существующего документа по id."""
d = db.get(LegalDocument, doc_id)
if not d:
raise HTTPException(status_code=404, detail="Document not found")
orch = LegalAnalysisOrchestrator(db=db)
out = orch.run(
document_id=doc_id,
jurisdiction_id=d.jurisdiction_id,
title=d.title,
content=d.content,
existing_document_type=d.document_type,
skip_agents=skip_agents,
save_cross_references=save_cross_references,
)
d.document_type = out["document_type"]
d.analysis_json = out.get("analysis_json")
d.compliance_notes = out.get("compliance_notes")
db.commit()
db.refresh(d)
return AnalysisResponse(
document_type=out["document_type"],
analysis_json=out.get("analysis_json"),
compliance_notes=out.get("compliance_notes"),
results=out.get("results", {}),
)
# -----------------------------------------------------------------------
# ДОПОЛНИТЕЛЬНЫЕ ФАП (№5-7 из перечня исходных документов)
# -----------------------------------------------------------------------
FAP_ADDITIONAL = {
"ФАП-148": {
"full_name": "Требования к эксплуатантам гражданских воздушных судов по обеспечению поддержания лётной годности",
"document": "Приказ Минтранса России от 23.06.2003 № 148",
"status": "Действует",
"scope": [
"Обязанности эксплуатанта по ПЛГ",
"Программа ТО воздушного судна",
"Контроль за выполнением директив лётной годности",
"Ведение эксплуатационной документации",
"Учёт наработки агрегатов и компонентов",
"Контроль назначенных ресурсов и сроков службы",
],
"relevance_to_system": "Базовый документ для модулей: Лётная годность, ТО, Чек-листы, Риски",
},
"ФАП-149": {
"full_name": "Требования к электросветотехническому обеспечению полётов",
"document": "Приказ Минтранса России от 23.06.2003 № 149",
"status": "Действует",
"scope": [
"Нормы электросветотехнического обеспечения на аэродромах",
"Требования к светосигнальному оборудованию",
"Контроль технического состояния электросветотехнических средств",
"Периодичность проверок и ТО",
],
"relevance_to_system": "Учитывается при аудитах аэродромной инфраструктуры и чек-листах",
},
"ФАП-10": {
"full_name": "Сертификационные требования к эксплуатантам коммерческой гражданской авиации",
"document": "Приказ Минтранса России от 04.02.2003 № 10 (ФАП-246 от 13.08.2015 — актуальная редакция)",
"status": "Заменён ФАП-246, но ряд положений действует",
"scope": [
"Организационная структура эксплуатанта",
"Требования к руководящему персоналу",
"Система управления безопасностью полётов",
"Программа подготовки авиационного персонала",
"Требования к парку ВС",
],
"relevance_to_system": "Базис для модуля Сертификация эксплуатантов (заявки, организации)",
},
}
@router.get("/fap-additional", tags=["legal"])
def get_additional_fap():
"""Дополнительные ФАП: 148, 149, 10."""
return FAP_ADDITIONAL
# -----------------------------------------------------------------------
# ПОРУЧЕНИЕ ПРЕЗИДЕНТА + ФЗ-488 + ВЫСТУПЛЕНИЕ КУДИНОВА + ТЗ АСУ ТК
# -----------------------------------------------------------------------
NATIONAL_PLG_FRAMEWORK = {
"presidential_order": {
"name": "Поручение Президента РФ Пр-1379 от 17.07.2019, п.2 пп.«в»",
"subject": "Гармонизация условий поддержания лётной годности ВС",
"requirements": [
"Создание национальной системы поддержания лётной годности",
"Гармонизация требований с международными стандартами (ICAO, EASA)",
"Обеспечение непрерывности контроля технического состояния ВС",
],
},
"fz_488": {
"name": "Федеральный закон от 30.12.2021 № 488-ФЗ",
"subject": "Введение статьи 37.2 ВК РФ «Поддержание лётной годности»",
"article_37_2": {
"text": "Поддержание лётной годности ВС — комплекс мер по обеспечению соответствия ВС "
"требованиям к лётной годности и поддержанию его безопасной эксплуатации",
"obligations": [
"Эксплуатант обязан обеспечивать ПЛГ",
"ФАВТ осуществляет государственный контроль за ПЛГ",
"Организация по ТО должна иметь сертификат",
],
},
},
"kudinov_speech": {
"name": "Выступление Кудинова В.В., начальника УПЛГ Росавиации",
"source": "Протокол Общественного совета ФАВТ от 21.10.2025 №14",
"key_points": [
"Необходимость создания национальной цифровой системы ПЛГ",
"Переход от бумажного документооборота к электронному",
"Интеграция с ФГИС РЭВС и другими информационными системами ФАВТ",
"Обеспечение прослеживаемости всех операций по ТО и ПЛГ",
"Автоматизация контроля сроков действия сертификатов и директив",
],
"relevance": "Определяет вектор развития АСУ ТК как элемента национальной системы ПЛГ",
},
"tz_asu_tk": {
"name": "Техническое задание на АСУ ТК «Контроль летной годности ВС»",
"approved_by": "Утверждено заместителем министра транспорта РФ 24.07.2022",
"scope": [
"Автоматизация процессов контроля лётной годности",
"Учёт воздушных судов и их технического состояния",
"Контроль выполнения программ ТО",
"Управление сертификационными процедурами",
"Мониторинг рисков безопасности полётов",
"Интеграция с системами ФАВТ и эксплуатантов",
"Обеспечение доступа для регулятора (ФАВТ) к агрегированным данным",
],
"system_modules_mapping": {
"Реестр ВС": "/aircraft, /airworthiness",
"Организации": "/organizations",
"Сертификация": "/applications, /regulator/certifications",
"Чек-листы и аудиты": "/checklists, /audits",
"Управление рисками": "/risks, /regulator/safety-indicators",
"Документооборот": "/documents, /inbox",
"Панель регулятора": "/regulator",
"Аналитика": "/analytics, /dashboard",
},
},
}
@router.get("/national-plg-framework", tags=["legal"])
def get_national_plg_framework():
"""
Национальная система ПЛГ — Поручение Президента, ФЗ-488,
выступление Кудинова, ТЗ АСУ ТК.
"""
return NATIONAL_PLG_FRAMEWORK
@router.get("/compliance-matrix", tags=["legal"])
def get_compliance_matrix():
"""
Матрица соответствия: 19 исходных документов → модули системы.
"""
return {
"system": "КЛГ АСУ ТК v16",
"developer": "АО «REFLY»",
"matrix": [
{"num": 1, "document": "Воздушный кодекс РФ (60-ФЗ)", "articles": "ст. 8, 35, 36, 37, 37.2",
"modules": ["Панель ФАВТ", "Сертификация", "Лётная годность", "Реестр ВС"],
"status": "implemented"},
{"num": 2, "document": "ФАП-21 (Часть 21)", "articles": "Приказ №184",
"modules": ["Сертификация АТ", "Организации"],
"status": "implemented"},
{"num": 3, "document": "ФАП-128", "articles": "Приказ №128",
"modules": ["Чек-листы полётов", "Аудиты"],
"status": "implemented"},
{"num": 4, "document": "ФАП-145 (Часть 145)", "articles": "Приказ №367",
"modules": ["ТО ВС", "Организации ТО", "Аудиты ТО"],
"status": "implemented"},
{"num": 5, "document": "ФАП-148", "articles": "Приказ №148",
"modules": ["Лётная годность", "Программы ТО", "Контроль ресурсов"],
"status": "implemented"},
{"num": 6, "document": "ФАП-149", "articles": "Приказ №149",
"modules": ["Аудиты инфраструктуры", "Чек-листы"],
"status": "implemented"},
{"num": 7, "document": "ФАП-10 / ФАП-246", "articles": "Приказ №10 / №246",
"modules": ["Сертификация эксплуатантов", "Организации"],
"status": "implemented"},
{"num": 8, "document": "ФАП-147", "articles": "Приказ №147",
"modules": ["Персонал ТО", "Квалификация экипажей"],
"status": "implemented"},
{"num": 9, "document": "EASA Part-M", "articles": "Reg. 1321/2014 Annex I",
"modules": ["Лётная годность", "Программы ТО"],
"status": "implemented"},
{"num": 10, "document": "EASA Part-CAMO", "articles": "Reg. 2019/1383",
"modules": ["Организации CAMO", "ПЛГ"],
"status": "implemented"},
{"num": 11, "document": "ICAO Annex 6", "articles": "Part I, Ch.8",
"modules": ["Сертификация", "ТО", "Чек-листы"],
"status": "implemented"},
{"num": 12, "document": "ICAO Annex 8", "articles": "4th edition",
"modules": ["Лётная годность", "Сертификация типа"],
"status": "implemented"},
{"num": 13, "document": "ICAO Doc 9760", "articles": "3rd edition",
"modules": ["Лётная годность", "Панель ФАВТ"],
"status": "implemented"},
{"num": 14, "document": "ICAO Annex 19", "articles": "2nd edition",
"modules": ["Управление рисками", "Безопасность полётов"],
"status": "implemented"},
{"num": 15, "document": "ICAO Doc 9734", "articles": "Part A, 3rd edition",
"modules": ["Панель ФАВТ", "Аудиты", "Надзор"],
"status": "implemented"},
{"num": 16, "document": "Поручение Президента Пр-1379", "articles": "п.2 пп.«в»",
"modules": ["Вся система — как элемент нац. системы ПЛГ"],
"status": "implemented"},
{"num": 17, "document": "ФЗ-488 (ст. 37.2 ВК)", "articles": "от 30.12.2021",
"modules": ["Лётная годность", "ПЛГ", "Контроль ФАВТ"],
"status": "implemented"},
{"num": 18, "document": "Выступление Кудинова В.В.", "articles": "Протокол ОС №14",
"modules": ["Цифровизация", "Интеграция ФГИС", "Электронный документооборот"],
"status": "implemented"},
{"num": 19, "document": "ТЗ АСУ ТК", "articles": "Утв. 24.07.2022",
"modules": ["Все модули системы"],
"status": "implemented"},
],
}