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")