# 📝 SQL запросы для n8n вебхуков ## PostgreSQL Connection: - **Host:** `147.45.189.234` - **Port:** `5432` - **Database:** `default_db` - **User:** `gen_user` - **Password:** `2~~9_^kVsU?2\S` --- ## 1️⃣ Создание заявки (при генерации claim_id) **Вебхук:** `POST /webhook/create-claim` **Input:** ```json { "claim_id": "CLM-2025-10-25-A3F7G2", "voucher": "E1000-302372730", "client_phone": "", "client_email": "", "session_id": "sess-abc-123" } ``` **SQL (PostgreSQL Node):** ```sql INSERT INTO claims ( claim_number, policy_number, client_phone, client_email, status, insurance_type, source, form_data, created_at ) VALUES ( '{{ $json.body.claim_id }}', '{{ $json.body.voucher }}', '{{ $json.body.client_phone || "" }}', '{{ $json.body.client_email || "" }}', 'draft', 'erv_travel', 'web_form', '{{ JSON.stringify($json.body) }}', NOW() ) ON CONFLICT (claim_number) DO UPDATE SET updated_at = NOW(), form_data = EXCLUDED.form_data RETURNING id, claim_number, created_at; ``` **Response:** ```json { "success": true, "claim_id": "CLM-2025-10-25-A3F7G2", "db_id": "uuid-from-db", "created_at": "2025-10-25T10:00:00Z" } ``` --- ## 2️⃣ Сохранение файла в claim_files **После S3 Upload в том же workflow!** **SQL (PostgreSQL Node после S3):** ```sql INSERT INTO claim_files ( claim_id, file_name, file_path, file_size, mime_type, file_type, s3_bucket, s3_key, s3_url, ocr_status, created_at ) SELECT c.id, '{{ $json.file.original_name }}', '{{ $json.s3.key }}', {{ $json.file.size || 0 }}, '{{ $json.file.mime_type }}', '{{ $json.claim.file_type }}', 'f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c', '{{ $json.s3.key }}', '{{ $('Upload a file1').item.json.Location }}', 'pending', NOW() FROM claims c WHERE c.claim_number = '{{ $json.claim.claim_id }}' RETURNING id as file_id, s3_url, ocr_status; ``` **Response (добавь в Respond):** ```json { "success": true, "claim_id": "CLM-2025-10-25-A3F7G2", "file": { "file_id": "uuid-from-db", "type": "policy_scan", "url": "https://s3.../policy_scan.pdf", "s3_key": "files/erv/ticket/CLM-xxx/policy_scan.pdf", "ocr_status": "pending" } } ``` --- ## 3️⃣ Обновление OCR результата **OCR Workflow (после обработки):** **SQL:** ```sql UPDATE claim_files SET ocr_status = 'completed', ocr_text = '{{ $json.ocr_text }}', processed_at = NOW() WHERE id = '{{ $json.file_id }}' RETURNING id, ocr_status; ``` --- ## 4️⃣ Обновление Vision AI результата **SQL:** ```sql UPDATE claim_files SET ai_extracted_data = '{{ JSON.stringify($json.ai_analysis) }}', processed_at = NOW() WHERE id = '{{ $json.file_id }}' RETURNING id, ai_extracted_data; ``` **Пример ai_extracted_data:** ```json { "document_type": "policy", "is_valid": true, "confidence": 0.95, "voucher": "E1000-302372730", "holder_name": "IVANOV IVAN", "insured_from": "01.11.2025", "insured_to": "30.11.2025" } ``` --- ## 5️⃣ Получить все файлы заявки **Вебхук:** `GET /webhook/get-claim-files/{claim_id}` **SQL:** ```sql SELECT cf.id, cf.file_name, cf.file_type, cf.s3_url, cf.file_size, cf.ocr_status, cf.ocr_text, cf.ai_extracted_data, cf.created_at, cf.processed_at FROM claim_files cf JOIN claims c ON c.id = cf.claim_id WHERE c.claim_number = '{{ $parameter.claim_id }}' ORDER BY cf.created_at; ``` **Response:** ```json { "success": true, "claim_id": "CLM-2025-10-25-A3F7G2", "files": [ { "file_id": "...", "file_type": "policy_scan", "s3_url": "...", "ocr_status": "completed", "ocr_text": "ЕВРОИНС...", "ai_extracted_data": {...} } ] } ``` --- ## 6️⃣ Финальная отправка заявки **SQL (обновляем статус):** ```sql UPDATE claims SET status = 'submitted', client_phone = '{{ $json.phone }}', client_email = '{{ $json.email }}', form_data = '{{ JSON.stringify($json.form_data) }}', submitted_at = NOW(), updated_at = NOW() WHERE claim_number = '{{ $json.claim_id }}' RETURNING id, claim_number, status, submitted_at; ``` --- ## 7️⃣ Публикация результатов OCR/Vision в Redis **После OCR/Vision обработки - отправляем результат в React через Redis Pub/Sub** ### Webhook для публикации: **POST** `http://147.45.189.234:8000/events/{claim_id}` **Headers:** ``` Content-Type: application/json ``` **Body (n8n Code Node):** ```json { "event_type": "ocr_completed", "status": "success", "data": { "file_id": "{{ $json.file_id }}", "file_type": "policy_scan", "is_valid_document": true, "document_type": "ERV Travel Insurance Policy", "ocr_text": "E1000-302372730", "confidence": 0.95, "ai_analysis": { "is_policy": true, "contains_policy_number": true, "is_nsfw": false, "warnings": [] } }, "message": "✅ Распознан полис страхования ERV", "timestamp": "{{ new Date().toISOString() }}" } ``` --- ### Code Node для валидации документа: **После OCR + Vision:** ```javascript // Получаем результаты OCR и Vision const ocrData = $json.ocr_result; // Из предыдущей ноды const visionData = $json.vision_result; // Валидация документа const validation = { is_valid_document: false, document_type: 'unknown', confidence: 0, warnings: [] }; // 1. Проверка на NSFW if (visionData.nsfw === true || visionData.nsfw_score > 0.7) { validation.warnings.push('Неподходящее содержимое изображения'); validation.is_valid_document = false; validation.document_type = 'inappropriate_content'; } // 2. Проверка текста OCR на наличие номера полиса const policyNumberRegex = /[A-Z]\d{4}-\d{9}/; const hasPolicyNumber = policyNumberRegex.test(ocrData.ocr_text); if (hasPolicyNumber) { validation.is_valid_document = true; validation.document_type = 'ERV Travel Insurance Policy'; validation.confidence = 0.9; } else { validation.warnings.push('Номер полиса не найден'); } // 3. Анализ Vision описания const visionText = visionData.content?.toLowerCase() || ''; const insuranceKeywords = ['страхов', 'insurance', 'полис', 'policy', 'erv']; const hasInsuranceKeywords = insuranceKeywords.some(kw => visionText.includes(kw)); if (hasInsuranceKeywords) { validation.confidence += 0.05; } else { validation.warnings.push('Документ не похож на страховой полис'); validation.is_valid_document = false; } // 4. Формируем результат для публикации в Redis const result = { file_id: $json.file_id, claim_id: $json.claim_id, event_type: 'ocr_completed', status: validation.is_valid_document ? 'success' : 'error', data: { file_id: $json.file_id, file_type: $json.file_type, is_valid_document: validation.is_valid_document, document_type: validation.document_type, ocr_text: ocrData.ocr_text, confidence: validation.confidence, ai_analysis: { is_policy: validation.is_valid_document, contains_policy_number: hasPolicyNumber, is_nsfw: visionData.nsfw, nsfw_score: visionData.nsfw_score, warnings: validation.warnings } }, message: validation.is_valid_document ? '✅ Распознан полис страхования ERV' : `❌ ${validation.warnings.join(', ')}`, timestamp: new Date().toISOString() }; return result; ``` --- ### HTTP Request Node (публикация в Redis): **Method:** `POST` **URL:** `http://147.45.189.234:8000/events/{{ $json.claim_id }}` **Headers:** ```json { "Content-Type": "application/json" } ``` **Body:** ```json {{ $json }} ``` --- ### React подписка на события: **Frontend код:** ```typescript useEffect(() => { if (!claimId) return; // Подключаемся к SSE const eventSource = new EventSource( `http://147.45.189.234:8000/events/${claimId}` ); eventSource.onmessage = (event) => { const data = JSON.parse(event.data); if (data.event_type === 'ocr_completed') { setUploadProgress(''); // Убираем крутилку if (data.status === 'success' && data.data.is_valid_document) { message.success(data.message); // ✅ Полис распознан - можно продолжать } else { message.error(data.message); // ❌ Это не полис - показываем предупреждение Modal.error({ title: 'Документ не распознан', content: data.data.ai_analysis.warnings.join('\n') }); } } }; return () => eventSource.close(); }, [claimId]); ``` --- ### Полный workflow в n8n: ``` Webhook (file upload) ↓ S3 Upload ↓ PostgreSQL (INSERT claim_files) ↓ OCR Service (HTTP Request) ↓ Vision Service (HTTP Request) ↓ Code Node (валидация документа) ↓ IF Node: is_valid_document? ├─ TRUE → PostgreSQL UPDATE (ocr_status = 'valid') │ ↓ │ HTTP POST → /events/{claim_id} (Redis Pub/Sub) │ ↓ │ Respond to Webhook: {success: true} │ └─ FALSE → PostgreSQL UPDATE (ocr_status = 'invalid') ↓ HTTP POST → /events/{claim_id} (Redis Pub/Sub) ↓ Respond to Webhook: {success: true, warning: true} ``` --- **Готово! Теперь делаем вебхуки в n8n?** 🚀