diff --git a/PROJECT_TIMELINE.md b/PROJECT_TIMELINE.md index 94853e3..fd67cc2 100644 --- a/PROJECT_TIMELINE.md +++ b/PROJECT_TIMELINE.md @@ -549,3 +549,4 @@ Last commit: c049ed6 - "fix: Добавлены n8n webhook URLs в docker-compo + diff --git a/TEST_ATTACH_DOCUMENT.md b/TEST_ATTACH_DOCUMENT.md new file mode 100644 index 0000000..16f941b --- /dev/null +++ b/TEST_ATTACH_DOCUMENT.md @@ -0,0 +1,129 @@ +# 📎 Тестирование привязки документов к проекту/заявке + +## Эндпоинт +``` +POST https://crm.clientright.ru/api/n8n/documents/attach +``` + +## 📋 Входные данные + +### Обязательные поля: +- `contact_id` - ID контакта +- `project_id` - ID проекта +- `file_url` - URL файла в S3 +- `file_name` - Имя файла + +### Опциональные поля: +- `ticket_id` - ID заявки (если указан → привязываем к заявке) +- `file_type` - Описание документа (например: "flight_delay_boarding_or_ticket") + +--- + +## 🧪 Тест 1: Привязка к проекту + +```bash +curl -X POST "https://crm.clientright.ru/api/n8n/documents/attach" \ + -H "Content-Type: application/json" \ + -d '{ + "contact_id": "320096", + "project_id": "396874", + "file_url": "https://s3.twcstorage.ru/f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c/clientright/test/test_document.pdf", + "file_name": "boarding_pass.pdf", + "file_type": "flight_delay_boarding_or_ticket" + }' +``` + +**Ожидаемый результат:** +```json +{ + "success": true, + "result": { + "document_id": "15x396940", + "document_numeric_id": "396940", + "attached_to": "project", + "attached_to_id": "396874", + "file_name": "boarding_pass.pdf", + "file_type": "flight_delay_boarding_or_ticket", + "s3_bucket": "f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c", + "s3_key": "clientright/test/test_document.pdf", + "file_size": 12345, + "message": "Документ создан с правильными S3 метаданными и привязан к проекту" + } +} +``` + +--- + +## 🧪 Тест 2: Привязка к заявке (HelpDesk) + +```bash +curl -X POST "https://crm.clientright.ru/api/n8n/documents/attach" \ + -H "Content-Type: application/json" \ + -d '{ + "contact_id": "320096", + "project_id": "396874", + "ticket_id": "396935", + "file_url": "https://s3.twcstorage.ru/f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c/clientright/test/test_document.pdf", + "file_name": "flight_delay_confirmation.pdf", + "file_type": "flight_delay_confirmation" + }' +``` + +**Ожидаемый результат:** +```json +{ + "success": true, + "result": { + "document_id": "15x396941", + "document_numeric_id": "396941", + "attached_to": "ticket", + "attached_to_id": "396935", + "file_name": "flight_delay_confirmation.pdf", + "file_type": "flight_delay_confirmation", + "s3_bucket": "f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c", + "s3_key": "clientright/test/test_document.pdf", + "file_size": 12345, + "message": "Документ создан с правильными S3 метаданными и привязан к проекту" + } +} +``` + +--- + +## 📊 Логика работы + +1. **Если `ticket_id` НЕ указан:** + - Документ создается в vTiger CRM (модуль Documents) + - Привязывается к **Project** (проекту) + - `attached_to = "project"` + +2. **Если `ticket_id` указан:** + - Документ создается в vTiger CRM (модуль Documents) + - Привязывается к **HelpDesk** (заявке) + - `attached_to = "ticket"` + +3. **S3 метаданные:** + - Автоматически обновляются в базе vTiger + - `filelocationtype = 'E'` (External URL) + - Сохраняются `s3_bucket`, `s3_key`, `file_size` + +--- + +## 🔍 Где смотреть логи + +### Backend логи: +```bash +docker-compose logs -f backend | grep "Attaching document" +``` + +### CRM логи: +```bash +tail -f /var/www/fastuser/data/www/crm.clientright.ru/logs/upload_documents.log +``` + +--- + +## ✅ Готово! + +Эндпоинт готов к интеграции в n8n workflow! + diff --git a/backend/app/api/n8n_proxy.py b/backend/app/api/n8n_proxy.py index b5a92dd..1862354 100644 --- a/backend/app/api/n8n_proxy.py +++ b/backend/app/api/n8n_proxy.py @@ -245,3 +245,126 @@ async def proxy_create_claim(request: Request): 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)}") +