Изменения: ✅ Новый endpoint: POST /api/n8n/documents/attach ✅ Поддерживает привязку к Project или HelpDesk ✅ Логика: если указан ticket_id → HelpDesk, иначе → Project ✅ Полное логирование всех операций ✅ Интеграция с upload_documents_to_crm.php Входные данные: - contact_id (обязательно) - project_id (обязательно) - file_url (обязательно) - file_name (обязательно) - ticket_id (опционально, для привязки к заявке) - file_type (опционально, описание документа) Готово к интеграции в n8n workflow!
371 lines
16 KiB
Python
371 lines
16 KiB
Python
"""
|
|
N8N Webhook Proxy Router
|
|
Безопасное проксирование запросов к n8n webhooks.
|
|
Frontend не знает прямых URL webhooks!
|
|
"""
|
|
import httpx
|
|
import logging
|
|
from fastapi import APIRouter, HTTPException, File, UploadFile, Form, Request
|
|
from fastapi.responses import JSONResponse
|
|
from typing import Optional
|
|
|
|
from ..config import settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/api/n8n", tags=["n8n-proxy"])
|
|
|
|
|
|
# URL webhooks из .env (будут добавлены)
|
|
N8N_POLICY_CHECK_WEBHOOK = getattr(settings, 'n8n_policy_check_webhook', None)
|
|
N8N_FILE_UPLOAD_WEBHOOK = getattr(settings, 'n8n_file_upload_webhook', None)
|
|
N8N_CREATE_CONTACT_WEBHOOK = getattr(settings, 'n8n_create_contact_webhook', 'https://n8n.clientright.pro/webhook/511fde97-88bb-4fb4-bea5-cafdc364be27')
|
|
N8N_CREATE_CLAIM_WEBHOOK = getattr(settings, 'n8n_create_claim_webhook', 'https://n8n.clientright.pro/webhook/d5bf4ca6-9e44-44b9-9714-3186ea703e7d')
|
|
|
|
|
|
@router.post("/policy/check")
|
|
async def proxy_policy_check(request: Request):
|
|
"""
|
|
Проксирует проверку полиса к n8n webhook
|
|
|
|
Frontend отправляет: POST /api/n8n/policy/check
|
|
Backend проксирует к: https://n8n.clientright.pro/webhook/{uuid}
|
|
"""
|
|
if not N8N_POLICY_CHECK_WEBHOOK:
|
|
raise HTTPException(status_code=500, detail="N8N webhook не настроен")
|
|
|
|
try:
|
|
# Получаем JSON body от фронтенда
|
|
body = await request.json()
|
|
|
|
logger.info(f"🔄 Proxy policy check: {body.get('policy_number', 'unknown')}")
|
|
|
|
# Проксируем запрос к n8n
|
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
response = await client.post(
|
|
N8N_POLICY_CHECK_WEBHOOK,
|
|
json=body,
|
|
headers={"Content-Type": "application/json"}
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
response_text = response.text
|
|
logger.info(f"✅ Policy check success. Response: {response_text[:500]}")
|
|
|
|
try:
|
|
return response.json()
|
|
except Exception as e:
|
|
logger.error(f"❌ Failed to parse JSON: {e}. Response: {response_text[:500]}")
|
|
raise HTTPException(status_code=500, detail=f"Ошибка парсинга ответа n8n: {str(e)}")
|
|
else:
|
|
logger.error(f"❌ N8N returned {response.status_code}: {response.text}")
|
|
raise HTTPException(
|
|
status_code=response.status_code,
|
|
detail=f"N8N error: {response.text}"
|
|
)
|
|
|
|
except httpx.TimeoutException:
|
|
logger.error("⏱️ N8N webhook timeout")
|
|
raise HTTPException(status_code=504, detail="Таймаут подключения к n8n")
|
|
except Exception as e:
|
|
logger.error(f"❌ Error proxying to n8n: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Ошибка проверки полиса: {str(e)}")
|
|
|
|
|
|
@router.post("/contact/create")
|
|
async def proxy_create_contact(request: Request):
|
|
"""
|
|
Проксирует создание контакта к n8n webhook
|
|
|
|
Frontend отправляет: POST /api/n8n/contact/create
|
|
Backend проксирует к: https://n8n.clientright.pro/webhook/511fde97-88bb-4fb4-bea5-cafdc364be27
|
|
"""
|
|
if not N8N_CREATE_CONTACT_WEBHOOK:
|
|
raise HTTPException(status_code=500, detail="N8N contact webhook не настроен")
|
|
|
|
try:
|
|
body = await request.json()
|
|
|
|
logger.info(f"🔄 Proxy create contact: phone={body.get('phone', 'unknown')}, session_id={body.get('session_id', 'unknown')}")
|
|
|
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
response = await client.post(
|
|
N8N_CREATE_CONTACT_WEBHOOK,
|
|
json=body,
|
|
headers={"Content-Type": "application/json"}
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
response_text = response.text
|
|
logger.info(f"✅ Contact created successfully. Response: {response_text[:500]}")
|
|
|
|
if not response_text or response_text.strip() == '':
|
|
logger.error(f"❌ N8N returned empty response")
|
|
raise HTTPException(status_code=500, detail="N8N вернул пустой ответ")
|
|
|
|
try:
|
|
return response.json()
|
|
except Exception as e:
|
|
logger.error(f"❌ Failed to parse JSON: {e}. Response: {response_text[:500]}")
|
|
raise HTTPException(status_code=500, detail=f"Ошибка парсинга ответа n8n: {str(e)}")
|
|
else:
|
|
logger.error(f"❌ N8N returned {response.status_code}: {response.text}")
|
|
raise HTTPException(
|
|
status_code=response.status_code,
|
|
detail=f"N8N error: {response.text}"
|
|
)
|
|
|
|
except httpx.TimeoutException:
|
|
logger.error("⏱️ N8N webhook timeout")
|
|
raise HTTPException(status_code=504, detail="Таймаут подключения к n8n")
|
|
except Exception as e:
|
|
logger.error(f"❌ Error proxying to n8n: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Ошибка создания контакта: {str(e)}")
|
|
|
|
|
|
@router.post("/upload/file")
|
|
async def proxy_file_upload(
|
|
file: UploadFile = File(...),
|
|
claim_id: Optional[str] = Form(None),
|
|
voucher: Optional[str] = Form(None),
|
|
session_id: Optional[str] = Form(None),
|
|
file_type: Optional[str] = Form(None),
|
|
filename: Optional[str] = Form(None),
|
|
upload_timestamp: Optional[str] = Form(None)
|
|
):
|
|
"""
|
|
Проксирует загрузку файла к n8n webhook
|
|
|
|
Frontend отправляет: POST /api/n8n/upload/file (multipart/form-data)
|
|
Backend проксирует к: https://n8n.clientright.pro/webhook/{uuid}
|
|
"""
|
|
if not N8N_FILE_UPLOAD_WEBHOOK:
|
|
raise HTTPException(status_code=500, detail="N8N upload webhook не настроен")
|
|
|
|
try:
|
|
logger.info(f"🔄 Proxy file upload: {file.filename} for claim {claim_id}")
|
|
|
|
# Читаем файл
|
|
file_content = await file.read()
|
|
|
|
# Формируем multipart/form-data для n8n
|
|
files = {
|
|
'file': (file.filename, file_content, file.content_type)
|
|
}
|
|
|
|
data = {}
|
|
if claim_id:
|
|
data['claim_id'] = claim_id
|
|
if voucher:
|
|
data['voucher'] = voucher
|
|
if session_id:
|
|
data['session_id'] = session_id
|
|
if file_type:
|
|
data['file_type'] = file_type
|
|
if filename:
|
|
data['filename'] = filename
|
|
if upload_timestamp:
|
|
data['upload_timestamp'] = upload_timestamp
|
|
|
|
# Проксируем запрос к n8n
|
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
response = await client.post(
|
|
N8N_FILE_UPLOAD_WEBHOOK,
|
|
files=files,
|
|
data=data
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
logger.info(f"✅ File upload success")
|
|
return response.json()
|
|
else:
|
|
logger.error(f"❌ N8N returned {response.status_code}: {response.text}")
|
|
raise HTTPException(
|
|
status_code=response.status_code,
|
|
detail=f"N8N error: {response.text}"
|
|
)
|
|
|
|
except httpx.TimeoutException:
|
|
logger.error("⏱️ N8N webhook timeout")
|
|
raise HTTPException(status_code=504, detail="Таймаут загрузки файла")
|
|
except Exception as e:
|
|
logger.error(f"❌ Error proxying file to n8n: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Ошибка загрузки файла: {str(e)}")
|
|
|
|
|
|
@router.post("/claim/create")
|
|
async def proxy_create_claim(request: Request):
|
|
"""
|
|
Проксирует создание черновика заявки к n8n webhook
|
|
|
|
Frontend отправляет: POST /api/n8n/claim/create
|
|
Backend проксирует к: https://n8n.clientright.pro/webhook/d5bf4ca6-9e44-44b9-9714-3186ea703e7d
|
|
"""
|
|
if not N8N_CREATE_CLAIM_WEBHOOK:
|
|
raise HTTPException(status_code=500, detail="N8N claim webhook не настроен")
|
|
|
|
try:
|
|
# Получаем JSON body от фронтенда
|
|
body = await request.json()
|
|
|
|
logger.info(f"🔄 Proxy create claim: event_type={body.get('event_type', 'unknown')}, claim_id={body.get('claim_id', 'unknown')}")
|
|
|
|
# Проксируем запрос к n8n
|
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
response = await client.post(
|
|
N8N_CREATE_CLAIM_WEBHOOK,
|
|
json=body,
|
|
headers={"Content-Type": "application/json"}
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
response_text = response.text
|
|
logger.info(f"✅ Claim created successfully. Response: {response_text[:200]}")
|
|
|
|
# Проверяем что ответ не пустой
|
|
if not response_text or response_text.strip() == '':
|
|
logger.error(f"❌ N8N returned empty response")
|
|
raise HTTPException(status_code=500, detail="N8N вернул пустой ответ")
|
|
|
|
try:
|
|
return response.json()
|
|
except Exception as e:
|
|
logger.error(f"❌ Failed to parse JSON: {e}. Response: {response_text[:500]}")
|
|
raise HTTPException(status_code=500, detail=f"Ошибка парсинга ответа n8n: {str(e)}")
|
|
else:
|
|
logger.error(f"❌ N8N returned {response.status_code}: {response.text}")
|
|
raise HTTPException(
|
|
status_code=response.status_code,
|
|
detail=f"N8N error: {response.text}"
|
|
)
|
|
|
|
except httpx.TimeoutException:
|
|
logger.error("⏱️ N8N webhook timeout")
|
|
raise HTTPException(status_code=504, detail="Таймаут подключения к n8n")
|
|
except Exception as e:
|
|
logger.error(f"❌ Error proxying to n8n: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Ошибка создания заявки: {str(e)}")
|
|
|
|
|
|
@router.post("/documents/attach")
|
|
async def attach_document_to_crm(request: Request):
|
|
"""
|
|
Привязывает загруженный файл к проекту или заявке в vTiger CRM
|
|
|
|
Входные данные:
|
|
- contact_id: ID контакта
|
|
- project_id: ID проекта (обязательно)
|
|
- ticket_id: ID заявки (опционально, если указан - привязываем к заявке)
|
|
- file_url: URL файла в S3
|
|
- file_name: Имя файла
|
|
- file_type: Тип файла (описание, например: "flight_delay_boarding_or_ticket")
|
|
|
|
Логика:
|
|
- Если указан ticket_id → привязываем к HelpDesk (заявке)
|
|
- Иначе → привязываем к Project (проекту)
|
|
"""
|
|
CRM_UPLOAD_ENDPOINT = "https://crm.clientright.ru/upload_documents_to_crm.php"
|
|
|
|
try:
|
|
body = await request.json()
|
|
|
|
contact_id = body.get('contact_id')
|
|
project_id = body.get('project_id')
|
|
ticket_id = body.get('ticket_id') # Опционально
|
|
file_url = body.get('file_url')
|
|
file_name = body.get('file_name')
|
|
file_type = body.get('file_type', 'Документ')
|
|
|
|
# Валидация обязательных полей
|
|
if not all([contact_id, project_id, file_url, file_name]):
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="Обязательные поля: contact_id, project_id, file_url, file_name"
|
|
)
|
|
|
|
logger.info(f"📎 Attaching document: {file_name} (type: {file_type})")
|
|
logger.info(f" Contact: {contact_id}, Project: {project_id}, Ticket: {ticket_id or 'N/A'}")
|
|
|
|
# Формируем payload для upload_documents_to_crm.php
|
|
upload_payload = {
|
|
"documents": [
|
|
{
|
|
"file_url": file_url,
|
|
"file_name": file_name,
|
|
"upload_description": file_type,
|
|
"contactid": int(contact_id),
|
|
"pages": 1
|
|
}
|
|
],
|
|
"projectid": int(project_id),
|
|
"ticket_id": int(ticket_id) if ticket_id else None, # Передаем ticket_id если есть
|
|
"user_id": 1
|
|
}
|
|
|
|
logger.info(f"📤 Sending to CRM: {upload_payload}")
|
|
|
|
# Отправляем запрос к CRM
|
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
response = await client.post(
|
|
CRM_UPLOAD_ENDPOINT,
|
|
json=upload_payload,
|
|
headers={"Content-Type": "application/json"}
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
response_text = response.text
|
|
logger.info(f"✅ Document attached successfully. Response: {response_text[:300]}")
|
|
|
|
try:
|
|
result = response.json()
|
|
|
|
# Проверяем успешность
|
|
if result.get('success') and result.get('results'):
|
|
first_result = result['results'][0]
|
|
|
|
if first_result.get('status') == 'success':
|
|
crm_result = first_result.get('crm_result', {})
|
|
|
|
return {
|
|
"success": True,
|
|
"result": {
|
|
"document_id": crm_result.get('document_id'),
|
|
"document_numeric_id": crm_result.get('document_numeric_id'),
|
|
"attached_to": "ticket" if ticket_id else "project",
|
|
"attached_to_id": ticket_id if ticket_id else project_id,
|
|
"file_name": file_name,
|
|
"file_type": file_type,
|
|
"s3_bucket": crm_result.get('s3_bucket'),
|
|
"s3_key": crm_result.get('s3_key'),
|
|
"file_size": crm_result.get('file_size'),
|
|
"message": crm_result.get('message')
|
|
}
|
|
}
|
|
else:
|
|
# Ошибка в CRM
|
|
error_msg = first_result.get('crm_result', {}).get('message', 'Unknown error')
|
|
logger.error(f"❌ CRM error: {error_msg}")
|
|
raise HTTPException(status_code=500, detail=f"CRM error: {error_msg}")
|
|
else:
|
|
logger.error(f"❌ Unexpected CRM response: {result}")
|
|
raise HTTPException(status_code=500, detail="Неожиданный ответ от CRM")
|
|
|
|
except Exception as e:
|
|
logger.error(f"❌ Failed to parse CRM response: {e}. Response: {response_text[:500]}")
|
|
raise HTTPException(status_code=500, detail=f"Ошибка парсинга ответа CRM: {str(e)}")
|
|
else:
|
|
logger.error(f"❌ CRM returned {response.status_code}: {response.text}")
|
|
raise HTTPException(
|
|
status_code=response.status_code,
|
|
detail=f"CRM error: {response.text}"
|
|
)
|
|
|
|
except httpx.TimeoutException:
|
|
logger.error("⏱️ CRM upload timeout")
|
|
raise HTTPException(status_code=504, detail="Таймаут загрузки в CRM")
|
|
except HTTPException:
|
|
raise # Пробрасываем HTTPException как есть
|
|
except Exception as e:
|
|
logger.error(f"❌ Error attaching document: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Ошибка привязки документа: {str(e)}")
|
|
|