klg-asutk-app/backend/app/api/routes/fgis_revs.py
Yuriy d9dd6d66cd refactor: legal package, personnel package, FGIS base_service, docs/SECURITY
- .gitignore: backend/venv/
- legal: routes/legal/ (base, handlers), legal_legacy.py
- personnel: routes/personnel/ re-exports personnel_plg
- FGIS: fgis/base_service.py, fgis_revs imports from fgis
- docs/SECURITY.md: security guide
- lib/logger.ts, logger-client.ts

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

334 lines
12 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 маршруты интеграции с ФГИС РЭВС.
Правовые основания:
- ВК РФ ст. 33 — Государственный реестр ГА ВС РФ
- ВК РФ ст. 36 — Сертификат лётной годности
- ВК РФ ст. 37.2 — Поддержание ЛГ (ФЗ-488)
- Приказ Росавиации № 180-П — ФГИС РЭВС
- ФАП-148 п.4.3 — информирование ФАВТ о выполнении ДЛГ
- ФАП-128 — обязательные донесения
"""
import logging
from datetime import datetime, timezone
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from app.api.deps import get_db, get_current_user
from app.api.helpers import audit
from app.services.fgis import SyncDirection
from app.services.fgis_revs import fgis_client
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/fgis-revs", tags=["fgis-revs"])
# ===================================================================
# PULL — получение данных из ФГИС РЭВС
# ===================================================================
@router.get("/aircraft-registry")
def get_fgis_aircraft(
registration: Optional[str] = None,
user=Depends(get_current_user),
):
"""
Запрос реестра ВС из ФГИС РЭВС.
ВК РФ ст. 33: государственный реестр ГА ВС РФ.
"""
aircraft = fgis_client.pull_aircraft_registry(registration)
return {
"source": "ФГИС РЭВС",
"legal_basis": "ВК РФ ст. 33; Приказ Минтранса № 98",
"total": len(aircraft),
"items": [a.__dict__ for a in aircraft],
}
@router.get("/certificates")
def get_fgis_certificates(
registration: Optional[str] = None,
user=Depends(get_current_user),
):
"""
Запрос СЛГ из ФГИС РЭВС.
ВК РФ ст. 36: удостоверение (сертификат) лётной годности.
"""
certs = fgis_client.pull_certificates(registration)
return {
"source": "ФГИС РЭВС",
"legal_basis": "ВК РФ ст. 36",
"total": len(certs),
"items": [c.__dict__ for c in certs],
}
@router.get("/operators")
def get_fgis_operators(user=Depends(get_current_user)):
"""
Реестр эксплуатантов из ФГИС РЭВС.
ФАП-246: сертификация эксплуатантов.
"""
operators = fgis_client.pull_operators()
return {
"source": "ФГИС РЭВС",
"legal_basis": "ВК РФ ст. 8; ФАП-246",
"total": len(operators),
"items": [o.__dict__ for o in operators],
}
@router.get("/directives")
def get_fgis_directives(
since_days: int = Query(30, ge=1, le=365),
user=Depends(get_current_user),
):
"""
Директивы ЛГ из ФГИС РЭВС за последние N дней.
ВК РФ ст. 37: обязательные ДЛГ.
"""
directives = fgis_client.pull_directives()
return {
"source": "ФГИС РЭВС",
"legal_basis": "ВК РФ ст. 37; ФАП-148 п.4.3",
"total": len(directives),
"items": [d.__dict__ for d in directives],
}
@router.get("/maintenance-organizations")
def get_fgis_maint_orgs(user=Depends(get_current_user)):
"""Реестр организаций по ТО (ФАП-145) из ФГИС РЭВС."""
orgs = fgis_client.pull_maint_organizations()
return {
"source": "ФГИС РЭВС",
"legal_basis": "ФАП-145",
"total": len(orgs),
"items": [o.__dict__ for o in orgs],
}
# ===================================================================
# PUSH — отправка данных в ФГИС РЭВС
# ===================================================================
class ComplianceReport(BaseModel):
directive_number: str
aircraft_registration: str
compliance_date: str
work_order_number: str
crs_signed_by: str
notes: str = ""
@router.post("/push/compliance-report")
def push_compliance(
data: ComplianceReport,
db: Session = Depends(get_db),
user=Depends(get_current_user),
):
"""
Отправить отчёт о выполнении ДЛГ в ФГИС РЭВС.
ФАП-148 п.4.3: эксплуатант обязан информировать ФАВТ.
"""
result = fgis_client.push_compliance_report(data.dict())
audit(db, user, "fgis_push", "compliance_report",
description=f"ДЛГ {data.directive_number} → ФГИС РЭВС: {result.get('status', '?')}")
db.commit()
return {
"action": "push_compliance",
"legal_basis": "ФАП-148 п.4.3",
"result": result,
}
class MaintenanceReport(BaseModel):
work_order_number: str
aircraft_registration: str
work_type: str
completion_date: str
crs_signed_by: str
actual_manhours: float = 0
findings: str = ""
@router.post("/push/maintenance-report")
def push_maintenance(
data: MaintenanceReport,
db: Session = Depends(get_db),
user=Depends(get_current_user),
):
"""
Отправить данные о выполненном ТО (CRS) в ФГИС РЭВС.
ФАП-145 п.A.55: документация о выполненном ТО.
"""
result = fgis_client.push_maintenance_report(data.dict())
audit(db, user, "fgis_push", "maintenance_report",
description=f"WO {data.work_order_number} → ФГИС РЭВС: {result.get('status', '?')}")
db.commit()
return {"action": "push_maintenance", "legal_basis": "ФАП-145 п.A.55", "result": result}
class DefectReport(BaseModel):
aircraft_registration: str
defect_description: str
severity: str
ata_chapter: str = ""
discovered_during: str = ""
@router.post("/push/defect-report")
def push_defect(
data: DefectReport,
db: Session = Depends(get_db),
user=Depends(get_current_user),
):
"""
Обязательное донесение о дефекте в ФАВТ через ФГИС РЭВС.
ФАП-128: обязательные донесения о событиях с ВС.
"""
result = fgis_client.push_defect_report(data.dict())
audit(db, user, "fgis_push", "defect_report",
description=f"Дефект {data.aircraft_registration} → ФГИС РЭВС")
db.commit()
return {"action": "push_defect", "legal_basis": "ФАП-128", "result": result}
# ===================================================================
# SYNC — синхронизация реестров
# ===================================================================
@router.post("/sync/aircraft")
def sync_aircraft(
background_tasks: BackgroundTasks,
db: Session = Depends(get_db),
user=Depends(get_current_user),
):
"""
Запустить синхронизацию реестра ВС с ФГИС РЭВС.
Фоновая задача — результат доступен через /sync/status.
"""
result = fgis_client.sync_aircraft()
audit(db, user, "fgis_sync", "aircraft",
description=f"Sync aircraft: {result.status} ({result.records_synced}/{result.records_total})")
db.commit()
return {
"action": "sync_aircraft",
"legal_basis": "ВК РФ ст. 33",
"result": result.__dict__,
}
@router.post("/sync/certificates")
def sync_certificates(
db: Session = Depends(get_db),
user=Depends(get_current_user),
):
"""Синхронизация СЛГ с ФГИС РЭВС."""
result = fgis_client.sync_certificates()
audit(db, user, "fgis_sync", "certificates",
description=f"Sync certs: {result.status}")
db.commit()
return {"action": "sync_certificates", "result": result.__dict__}
@router.post("/sync/directives")
def sync_directives(
since_days: int = Query(30, ge=1, le=365),
db: Session = Depends(get_db),
user=Depends(get_current_user),
):
"""
Синхронизация директив ЛГ с ФГИС РЭВС.
Автоматически создаёт новые AD в системе.
"""
result = fgis_client.sync_directives(since_days)
audit(db, user, "fgis_sync", "directives",
description=f"Sync AD: {result.status} ({result.records_synced} synced)")
db.commit()
return {"action": "sync_directives", "legal_basis": "ВК РФ ст. 37", "result": result.__dict__}
@router.post("/sync/all")
def sync_all(
db: Session = Depends(get_db),
user=Depends(get_current_user),
):
"""Полная синхронизация всех реестров с ФГИС РЭВС."""
results = {
"aircraft": fgis_client.sync_aircraft().__dict__,
"certificates": fgis_client.sync_certificates().__dict__,
"directives": fgis_client.sync_directives().__dict__,
}
audit(db, user, "fgis_sync", "all", description="Full ФГИС РЭВС sync")
db.commit()
return {"action": "sync_all", "results": results}
@router.get("/sync/status")
def sync_status(user=Depends(get_current_user)):
"""История и статус синхронизаций."""
log = fgis_client.get_sync_log()
return {
"total_syncs": len(log),
"last_sync": log[-1] if log else None,
"history": log[-20:],
}
# ===================================================================
# СМЭВ 3.0 — юридически значимый обмен
# ===================================================================
class SMEVRequest(BaseModel):
service_code: str = Field(..., description="FAVT-001..004")
data: dict = Field(default={})
@router.post("/smev/send")
def smev_send(
req: SMEVRequest,
db: Session = Depends(get_db),
user=Depends(get_current_user),
):
"""
Отправить запрос через СМЭВ 3.0 (юридически значимый обмен).
Требуется УКЭП (ГОСТ Р 34.10-2012).
Сервисы:
- FAVT-001: Запрос данных из реестра ВС
- FAVT-002: Подача заявления на СЛГ
- FAVT-003: Уведомление о выполнении ДЛГ
- FAVT-004: Отчёт о ТО
"""
message_id = fgis_client.smev_send_request(req.service_code, req.data)
audit(db, user, "smev_send", req.service_code,
description=f"СМЭВ 3.0: {req.service_code}, msg_id={message_id}")
db.commit()
return {
"action": "smev_send",
"service_code": req.service_code,
"message_id": message_id,
"note": "Ответ будет доступен асинхронно через /smev/responses",
}
@router.get("/connection-status")
def connection_status(user=Depends(get_current_user)):
"""Статус подключения к ФГИС РЭВС и СМЭВ."""
return {
"fgis_revs": {
"url": fgis_client.config.BASE_URL,
"status": "mock_mode",
"note": "Тестовая среда — используются mock-данные. Для production: настроить сертификат ГОСТ.",
},
"smev_30": {
"url": fgis_client.config.SMEV_URL,
"status": "mock_mode",
"note": "Требуется УКЭП и регистрация в СМЭВ 3.0.",
},
"config": {
"org_id": fgis_client.config.ORG_ID or "(не задан)",
"cert_path": fgis_client.config.CERT_PATH,
"timeout": fgis_client.config.TIMEOUT,
"max_retries": fgis_client.config.MAX_RETRIES,
},
}