diff --git a/DOCUMENT_ATTACH_API.md b/DOCUMENT_ATTACH_API.md index c3db4f7..4f7949e 100644 --- a/DOCUMENT_ATTACH_API.md +++ b/DOCUMENT_ATTACH_API.md @@ -12,21 +12,47 @@ POST https://crm.clientright.ru/api/n8n/documents/attach --- -## 📋 Параметры запроса +## 📋 Формат входных данных -### Обязательные: -| Параметр | Тип | Описание | Пример | -|----------|-----|----------|--------| -| `contact_id` | string | ID контакта в vTiger | `"320096"` | -| `project_id` | string | ID проекта (полиса) в vTiger | `"396874"` | -| `file_url` | string | Полный URL файла в S3 | `"https://s3.twcstorage.ru/..."` | -| `file_name` | string | Имя файла | `"boarding_pass.pdf"` | +**Тип:** JSON массив документов -### Опциональные: -| Параметр | Тип | Описание | Пример | -|----------|-----|----------|--------| -| `ticket_id` | string | ID заявки в HelpDesk
**Если указан → привязка к заявке**
**Если не указан → привязка к проекту** | `"396935"` | -| `file_type` | string | Описание типа документа | `"flight_delay_boarding_or_ticket"` | +```json +[ + { + "claim_id": "CLM-2025-11-02-WNRZZZ", + "event_type": "delay_flight", + "contact_id": "320096", + "project_id": "396868", + "ticket_id": "396936", + "filename": "boarding_pass.pdf", + "file_type": "flight_delay_boarding_or_ticket", + "file": "/bucket/path/to/file.pdf" + } +] +``` + +### Поля документа: + +| Параметр | Тип | Обязательно | Описание | Пример | +|----------|-----|-------------|----------|--------| +| `contact_id` | string | ✅ Да | ID контакта в vTiger | `"320096"` | +| `project_id` | string | ✅ Да | ID проекта (полиса) в vTiger | `"396874"` | +| `file` или `file_url` | string | ✅ Да | Путь к файлу в S3 (с/без хоста) | `"/bucket/path/file.pdf"` | +| `filename` или `file_name` | string | ✅ Да | Имя файла | `"boarding_pass.pdf"` | +| `ticket_id` | string | ⚠️ Опц. | ID заявки
**Если указан → HelpDesk**
**Если НЕ указан → Project** | `"396935"` | +| `file_type` | string | ⚠️ Опц. | Тип документа | `"flight_delay_boarding_or_ticket"` | +| `claim_id` | string | ⚠️ Опц. | ID заявки (для логирования) | `"CLM-2025-11-02-..."` | +| `event_type` | string | ⚠️ Опц. | Тип события (для логирования) | `"delay_flight"` | + +### 🔧 Умная обработка путей: + +Эндпоинт автоматически определяет формат пути и добавляет хост S3 если нужно: + +| Входной формат | Обработка | Результат | +|----------------|-----------|-----------| +| `/bucket/path/file.pdf` | ➕ Добавляем хост | `https://s3.twcstorage.ru/bucket/path/file.pdf` | +| `bucket/path/file.pdf` | ➕ Добавляем `/` и хост | `https://s3.twcstorage.ru/bucket/path/file.pdf` | +| `https://s3.twcstorage.ru/...` | ✅ Уже полный URL | `https://s3.twcstorage.ru/...` | --- @@ -58,76 +84,137 @@ POST https://crm.clientright.ru/api/n8n/documents/attach ## 📤 Примеры запросов -### 1️⃣ Привязка к проекту (без заявки) - -Используется когда файл относится к полису в целом, но еще нет конкретной заявки. +### 1️⃣ Один документ к заявке (реальный пример) ```bash curl -X POST "https://crm.clientright.ru/api/n8n/documents/attach" \ -H "Content-Type: application/json" \ - -d '{ + -d '[ + { + "claim_id": "CLM-2025-11-02-WNRZZZ", + "event_type": "delay_flight", "contact_id": "320096", - "project_id": "396874", - "file_url": "https://s3.twcstorage.ru/f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c/clientright/test/policy_scan.pdf", - "file_name": "policy_E1000-302538529.pdf", - "file_type": "Скан полиса ERV" - }' + "project_id": "396868", + "ticket_id": "396936", + "filename": "flight_delay_boarding_or_ticket.pdf", + "file_type": "flight_delay_boarding_or_ticket", + "file": "/f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c/crm2/CRM_Active_Files/Documents/HelpDesk/ЗАЯВКА_827_396936/flight_delay_boarding_or_ticket.pdf" + } +]' ``` **Ответ:** ```json { "success": true, - "result": { - "document_id": "15x396940", - "document_numeric_id": "396940", - "attached_to": "project", // ✅ Привязан к проекту - "attached_to_id": "396874", - "file_name": "policy_E1000-302538529.pdf", - "file_type": "Скан полиса ERV", - "s3_bucket": "f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c", - "s3_key": "clientright/test/policy_scan.pdf", - "file_size": 125840, - "message": "Документ создан с правильными S3 метаданными и привязан к проекту" - } + "total_processed": 1, + "successful": 1, + "failed": 0, + "results": [ + { + "document_id": "15x396941", + "document_numeric_id": "396941", + "attached_to": "ticket", + "attached_to_id": "396936", + "file_name": "flight_delay_boarding_or_ticket.pdf", + "file_type": "flight_delay_boarding_or_ticket", + "s3_bucket": "f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c", + "s3_key": "crm2/CRM_Active_Files/Documents/HelpDesk/ЗАЯВКА_827_396936/flight_delay_boarding_or_ticket.pdf", + "file_size": 85320, + "message": "Документ создан с правильными S3 метаданными и привязан к проекту" + } + ], + "errors": null } ``` --- -### 2️⃣ Привязка к заявке (HelpDesk) - -Используется когда файл относится к конкретной заявке (страховому случаю). +### 2️⃣ Несколько документов за раз (batch) ```bash curl -X POST "https://crm.clientright.ru/api/n8n/documents/attach" \ -H "Content-Type: application/json" \ - -d '{ + -d '[ + { "contact_id": "320096", - "project_id": "396874", - "ticket_id": "396935", // ✅ Указан ticket_id - "file_url": "https://s3.twcstorage.ru/f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c/clientright/test/boarding_pass.pdf", - "file_name": "boarding_pass_20251102.pdf", - "file_type": "flight_delay_boarding_or_ticket" - }' + "project_id": "396868", + "ticket_id": "396936", + "filename": "boarding_pass.pdf", + "file_type": "flight_delay_boarding_or_ticket", + "file": "/bucket/path/boarding_pass.pdf" + }, + { + "contact_id": "320096", + "project_id": "396868", + "ticket_id": "396936", + "filename": "delay_confirmation.pdf", + "file_type": "flight_delay_confirmation", + "file": "/bucket/path/delay_confirmation.pdf" + } +]' ``` **Ответ:** ```json { "success": true, - "result": { - "document_id": "15x396941", - "document_numeric_id": "396941", - "attached_to": "ticket", // ✅ Привязан к заявке - "attached_to_id": "396935", - "file_name": "boarding_pass_20251102.pdf", - "file_type": "flight_delay_boarding_or_ticket", - "s3_bucket": "f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c", - "s3_key": "clientright/test/boarding_pass.pdf", - "file_size": 85320, - "message": "Документ создан с правильными S3 метаданными и привязан к проекту" + "total_processed": 2, + "successful": 2, + "failed": 0, + "results": [ + { + "document_id": "15x396941", + "attached_to": "ticket", + "file_name": "boarding_pass.pdf", + "...": "..." + }, + { + "document_id": "15x396942", + "attached_to": "ticket", + "file_name": "delay_confirmation.pdf", + "...": "..." + } + ], + "errors": null +} +``` + +--- + +### 3️⃣ Привязка к проекту (без ticket_id) + +```bash +curl -X POST "https://crm.clientright.ru/api/n8n/documents/attach" \ + -H "Content-Type: application/json" \ + -d '[ + { + "contact_id": "320096", + "project_id": "396874", + "filename": "policy_scan.pdf", + "file_type": "Скан полиса ERV", + "file": "https://s3.twcstorage.ru/bucket/path/policy.pdf" } +]' +``` + +**Ответ:** +```json +{ + "success": true, + "total_processed": 1, + "successful": 1, + "failed": 0, + "results": [ + { + "document_id": "15x396940", + "attached_to": "project", + "attached_to_id": "396874", + "file_name": "policy_scan.pdf", + "...": "..." + } + ], + "errors": null } ``` diff --git a/TEST_QUICK.sh b/TEST_QUICK.sh new file mode 100755 index 0000000..c0cef1e --- /dev/null +++ b/TEST_QUICK.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# Быстрый тест привязки документа + +echo "🧪 Тест 1: Привязка к проекту (БЕЗ заявки)" +echo "==========================================" +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_doc.pdf", + "file_name": "test_project_doc.pdf", + "file_type": "Тестовый документ для проекта" + }' | jq . + +echo "" +echo "" +echo "🧪 Тест 2: Привязка к заявке (С ticket_id)" +echo "==========================================" +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_doc.pdf", + "file_name": "test_ticket_doc.pdf", + "file_type": "flight_delay_boarding_or_ticket" + }' | jq . + +echo "" +echo "✅ Тесты завершены!" + diff --git a/TEST_REAL_DATA.sh b/TEST_REAL_DATA.sh new file mode 100755 index 0000000..0384187 --- /dev/null +++ b/TEST_REAL_DATA.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# Тест с реальными данными из n8n + +echo "🧪 Тест привязки документа с реальными данными" +echo "==============================================" + +curl -X POST "https://crm.clientright.ru/api/n8n/documents/attach" \ + -H "Content-Type: application/json" \ + -d '[ + { + "claim_id": "CLM-2025-11-02-WNRZZZ", + "event_type": "delay_flight", + "contact_id": "320096", + "project_id": "396868", + "ticket_id": "396936", + "filename": "flight_delay_boarding_or_ticket.pdf", + "file_type": "flight_delay_boarding_or_ticket", + "file": "/f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c/crm2/CRM_Active_Files/Documents/HelpDesk/ЗАЯВКА_827_396936/flight_delay_boarding_or_ticket.pdf" + } +]' | jq . + +echo "" +echo "✅ Тест завершен!" +echo "" +echo "Ожидаемый результат:" +echo " - success: true" +echo " - total_processed: 1" +echo " - successful: 1" +echo " - attached_to: ticket" +echo " - attached_to_id: 396936" + diff --git a/backend/app/api/n8n_proxy.py b/backend/app/api/n8n_proxy.py index 1862354..83ccbb5 100644 --- a/backend/app/api/n8n_proxy.py +++ b/backend/app/api/n8n_proxy.py @@ -249,55 +249,96 @@ async def proxy_create_claim(request: Request): @router.post("/documents/attach") async def attach_document_to_crm(request: Request): """ - Привязывает загруженный файл к проекту или заявке в vTiger CRM + Привязывает загруженные файлы к проекту или заявке в vTiger CRM - Входные данные: - - contact_id: ID контакта - - project_id: ID проекта (обязательно) - - ticket_id: ID заявки (опционально, если указан - привязываем к заявке) - - file_url: URL файла в S3 - - file_name: Имя файла - - file_type: Тип файла (описание, например: "flight_delay_boarding_or_ticket") + Входные данные (массив документов): + [ + { + "claim_id": "CLM-2025-11-02-WNRZZZ", + "contact_id": "320096", + "project_id": "396868", + "ticket_id": "396936", // Опционально + "filename": "boarding_pass.pdf", + "file_type": "flight_delay_boarding_or_ticket", + "file": "/bucket/path/to/file.pdf" // Без хоста, добавим https://s3.twcstorage.ru + } + ] Логика: - Если указан ticket_id → привязываем к HelpDesk (заявке) - Иначе → привязываем к Project (проекту) """ CRM_UPLOAD_ENDPOINT = "https://crm.clientright.ru/upload_documents_to_crm.php" + S3_HOST = "https://s3.twcstorage.ru" 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', 'Документ') + # Поддерживаем как массив, так и одиночный объект + documents_array = body if isinstance(body, list) else [body] - # Валидация обязательных полей - 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 {len(documents_array)} document(s)") - 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'}") + # Обрабатываем каждый документ + processed_documents = [] + for idx, doc in enumerate(documents_array): + contact_id = doc.get('contact_id') + project_id = doc.get('project_id') + ticket_id = doc.get('ticket_id') # Опционально + + # Поддерживаем оба формата: file_url и file + file_path = doc.get('file') or doc.get('file_url') + if not file_path: + raise HTTPException( + status_code=400, + detail=f"Document #{idx}: отсутствует поле 'file' или 'file_url'" + ) + + # Строим полный S3 URL если это путь без хоста + if file_path.startswith('/'): + file_url = S3_HOST + file_path + elif not file_path.startswith('http'): + file_url = S3_HOST + '/' + file_path + else: + file_url = file_path + + # Поддерживаем оба формата: file_name и filename + file_name = doc.get('filename') or doc.get('file_name') + if not file_name: + raise HTTPException( + status_code=400, + detail=f"Document #{idx}: отсутствует поле 'filename' или 'file_name'" + ) + + file_type = doc.get('file_type', 'Документ') + + # Валидация обязательных полей + if not all([contact_id, project_id]): + raise HTTPException( + status_code=400, + detail=f"Document #{idx}: обязательные поля: contact_id, project_id" + ) + + logger.info(f" [{idx+1}/{len(documents_array)}] {file_name} (type: {file_type})") + logger.info(f" Contact: {contact_id}, Project: {project_id}, Ticket: {ticket_id or 'N/A'}") + logger.info(f" File URL: {file_url}") + + processed_documents.append({ + "file_url": file_url, + "file_name": file_name, + "upload_description": file_type, + "contactid": int(contact_id), + "pages": 1 + }) + + # Берем общие параметры из первого документа + first_doc = documents_array[0] # Формируем 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 если есть + "documents": processed_documents, + "projectid": int(first_doc.get('project_id')), + "ticket_id": int(first_doc.get('ticket_id')) if first_doc.get('ticket_id') else None, "user_id": 1 } @@ -320,31 +361,53 @@ async def attach_document_to_crm(request: Request): # Проверяем успешность if result.get('success') and result.get('results'): - first_result = result['results'][0] + results_array = result['results'] - if first_result.get('status') == 'success': - crm_result = first_result.get('crm_result', {}) - - return { - "success": True, - "result": { + # Обрабатываем результаты для каждого документа + processed_results = [] + errors = [] + + for idx, res in enumerate(results_array): + if res.get('status') == 'success': + crm_result = res.get('crm_result', {}) + + processed_results.append({ "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, + "attached_to": "ticket" if res.get('ticket_id') else "project", + "attached_to_id": res.get('ticket_id') or res.get('projectid'), + "file_name": res.get('file_name'), + "file_type": res.get('description'), "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') - } + }) + + logger.info(f" ✅ [{idx+1}] {res.get('file_name')} → {crm_result.get('document_id')}") + else: + # Ошибка для конкретного документа + error_msg = res.get('crm_result', {}).get('message', 'Unknown error') + errors.append({ + "file_name": res.get('file_name'), + "error": error_msg + }) + logger.error(f" ❌ [{idx+1}] {res.get('file_name')}: {error_msg}") + + # Если есть хотя бы один успешный результат - считаем успехом + if processed_results: + return { + "success": True, + "total_processed": len(results_array), + "successful": len(processed_results), + "failed": len(errors), + "results": processed_results, + "errors": errors if errors else None } 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}") + # Все документы упали с ошибкой + logger.error(f"❌ All documents failed: {errors}") + raise HTTPException(status_code=500, detail=f"Все документы не удалось привязать: {errors}") else: logger.error(f"❌ Unexpected CRM response: {result}") raise HTTPException(status_code=500, detail="Неожиданный ответ от CRM")