diff --git a/storage/2025/November/week5/399441_Ходатайство_по_делу_.pdf b/storage/2025/November/week5/399441_Ходатайство_по_делу_.pdf new file mode 100644 index 00000000..e7cc136f Binary files /dev/null and b/storage/2025/November/week5/399441_Ходатайство_по_делу_.pdf differ diff --git a/storage/2025/November/week5/399445_1_заявление_потребителя_Селдушев____стр.pdf b/storage/2025/November/week5/399445_1_заявление_потребителя_Селдушев____стр.pdf new file mode 100644 index 00000000..d6573fdf Binary files /dev/null and b/storage/2025/November/week5/399445_1_заявление_потребителя_Селдушев____стр.pdf differ diff --git a/test/LanguageManager/Workflow2 b/test/LanguageManager/Workflow2 index 260a6f32..69f03b14 100644 --- a/test/LanguageManager/Workflow2 +++ b/test/LanguageManager/Workflow2 @@ -1 +1 @@ -2025-11-28 14:40:08 \ No newline at end of file +2025-11-29 14:45:09 \ No newline at end of file diff --git a/ticket_form/backend/app/api/documents.py b/ticket_form/backend/app/api/documents.py index daee5895..4adf5d1b 100644 --- a/ticket_form/backend/app/api/documents.py +++ b/ticket_form/backend/app/api/documents.py @@ -611,4 +611,74 @@ async def generate_documents_list(request: Request): +@router.post("/check-ocr-status") +async def check_ocr_status(request: Request): + """ + Проверка статуса OCR обработки документов. + + Вызывается при нажатии "Продолжить" после загрузки документов. + Пушит запрос в Redis канал clpr:check:ocr_status, который слушает n8n workflow. + n8n проверяет статус всех документов и публикует результат в ocr_events:{session_id}. + """ + try: + body = await request.json() + + claim_id = body.get("claim_id") + session_id = body.get("session_id") + + if not claim_id or not session_id: + raise HTTPException( + status_code=400, + detail="claim_id и session_id обязательны", + ) + + logger.info( + "🔍 Check OCR status request", + extra={ + "claim_id": claim_id, + "session_id": session_id, + }, + ) + + # Публикуем запрос в Redis для n8n workflow + event_data = { + "claim_id": claim_id, + "session_token": session_id, + "timestamp": datetime.utcnow().isoformat(), + } + + channel = "clpr:check:ocr_status" + + subscribers = await redis_service.publish( + channel, + json.dumps(event_data, ensure_ascii=False) + ) + + logger.info( + "✅ OCR status check published", + extra={ + "channel": channel, + "subscribers": subscribers, + "claim_id": claim_id, + }, + ) + + return { + "success": True, + "message": "Запрос на проверку OCR статуса отправлен", + "channel": channel, + "listen_channel": f"ocr_events:{session_id}", + } + + except HTTPException: + raise + + except Exception as e: + logger.exception("❌ Error checking OCR status") + raise HTTPException( + status_code=500, + detail=f"Ошибка проверки статуса: {str(e)}", + ) + + router.add_api_route("/skip", skip_document, methods=["POST"], tags=["Documents"]) diff --git a/ticket_form/docs/SESSION_LOG_2025-11-29_RAG_WORKFLOW.md b/ticket_form/docs/SESSION_LOG_2025-11-29_RAG_WORKFLOW.md new file mode 100644 index 00000000..056ea3f4 --- /dev/null +++ b/ticket_form/docs/SESSION_LOG_2025-11-29_RAG_WORKFLOW.md @@ -0,0 +1,433 @@ +# Лог сессии: Настройка RAG workflow для извлечения данных +**Дата:** 2025-11-29 +**Workflow ID:** `itX62h38faB51y9J` ("6 ocr_check:attempt") + +--- + +## 🎯 Цель сессии + +Настроить workflow для автоматического извлечения данных из документов с использованием RAG (Retrieval-Augmented Generation) для заполнения формы заявления. + +--- + +## 📊 Структура workflow + +``` +ocr_check:attempt (Redis Trigger) + ↓ + clime_id (Set) → извлекает claim_id, session_id + ↓ + analiz (Set) → добавляет prefix, session_token + ↓ + give_data1 (PostgreSQL) → большой SQL, собирает все данные + ↓ + Code1 (Code) → нормализует данные + ↓ + prepare_rag_items (Code) → создаёт 3 items: user, project, offenders + ↓ + Loop Over Items → итерация по типам + ↓ + Code6 (Code) → генерация промптов для AI + ↓ + AI Agent2 (LLM + RAG) → извлечение данных из документов + ↓ + Code5 (Code) → парсинг JSON из LLM + ↓ + Edit Fields4 → извлекает output + ↓ + Aggregate → собирает все результаты + ↓ + dataset (Set) → финальная сборка +``` + +--- + +## 📝 Обновлённые ноды + +### 1. Code1 — нормализация данных из give_data1 + +```javascript +// Code1 — нормализация данных из give_data1 +// ИСПРАВЛЕНО: извлекаем payload.applicant, ai_analysis, wizard_plan, полные documents + +function toNullish(v) { + if (v === undefined || v === null) return null; + if (typeof v === 'string' && v.trim() === '') return null; + return v; +} + +function pick(...vals) { + return vals.find(v => v !== undefined && v !== null && v !== '') ?? null; +} + +function mapDocuments(docs = []) { + if (!docs || !Array.isArray(docs)) return []; + return docs.map(d => ({ + id: toNullish(d.id), + claim_document_id: toNullish(d.id), + file_id: toNullish(d.file_id), + file_url: toNullish(d.file_url), + file_name: toNullish(d.file_name), + original_file_name: toNullish(d.original_file_name), + field_name: toNullish(d.field_name), + uploaded_at: toNullish(d.uploaded_at), + filename_for_upload: toNullish(d.filename_for_upload), + // AI данные + document_type: toNullish(d.document_type), + document_label: toNullish(d.document_label), + document_summary: toNullish(d.document_summary), + ocr_status: toNullish(d.ocr_status), + match_score: toNullish(d.match_score), + match_status: toNullish(d.match_status), + match_reason: toNullish(d.match_reason), + })); +} + +function mapCombinedDocs(cds = []) { + if (!cds || !Array.isArray(cds)) return []; + return cds.map(c => ({ + claim_document_id: toNullish(c.claim_document_id), + combined_document_id: toNullish(c.combined_document_id), + pages: toNullish(c.pages), + combined_text: toNullish(c.combined_text), + })); +} + +function normalizeOne(src) { + const claim = src.claim ?? {}; + const payload = claim.payload ?? {}; + const userInfo = src.user_info ?? {}; + + // Извлекаем applicant из payload + const applicant = payload.applicant ?? {}; + const aiAnalysis = payload.ai_analysis ?? {}; + const answersPrefill = payload.answers_prefill ?? []; + const wizardPlan = payload.wizard_plan ?? null; + + // USER: приоритет payload.applicant + const user = { + firstname: pick(applicant.firstname, applicant.first_name), + secondname: pick(applicant.middle_name, applicant.secondname), + lastname: pick(applicant.lastname, applicant.last_name), + mobile: pick(payload.phone), + email: pick(payload.email), + tgid: pick(claim.telegram_id, payload.tg_id), + birthday: pick(applicant.birthday, applicant.birth_date), + birthplace: pick(applicant.birthplace, applicant.birth_place), + mailingstreet: pick(applicant.address), + inn: pick(applicant.inn), + zip: pick(applicant.zip), + channel: pick(userInfo.channel, claim.channel), + unified_id: pick(claim.unified_id), + session_token: pick(claim.session_token), + }; + + // CASE + const caseData = { + id: toNullish(claim.id), + prefix: toNullish(claim.prefix), + channel: toNullish(claim.channel), + type_code: toNullish(claim.type_code), + status_code: toNullish(claim.status_code), + created_at: toNullish(claim.created_at), + updated_at: toNullish(claim.updated_at), + telegram_id: toNullish(claim.telegram_id), + session_token: toNullish(claim.session_token), + unified_id: toNullish(claim.unified_id), + case_type: pick(wizardPlan?.case_type, claim.type_code), + }; + + // ANSWERS + const answers = {}; + if (Array.isArray(answersPrefill)) { + answersPrefill.forEach(a => { + if (a?.name && a?.value !== undefined) { + answers[a.name] = a.value; + } + }); + } + + // AI_ANALYSIS + const ai = { + problem: toNullish(aiAnalysis.problem), + facts_short: toNullish(aiAnalysis.facts_short), + facts_full: toNullish(aiAnalysis.facts_full), + recommendation: toNullish(aiAnalysis.recommendation), + }; + + const problemDescription = toNullish(payload.problem_description); + + return { + case: caseData, + user, + answers: Object.keys(answers).length ? answers : null, + answers_prefill: answersPrefill.length ? answersPrefill : null, + ai_analysis: ai, + problem_description: problemDescription, + documents: mapDocuments(src.documents), + combined_docs: mapCombinedDocs(src.combined_docs), + wizard_plan: wizardPlan, + meta: { + claim_id: caseData.id, + session_token: caseData.session_token, + unified_id: caseData.unified_id, + } + }; +} + +const raw = items[0]?.json ?? {}; +const arr = Array.isArray(raw) ? raw : [raw]; +const results = arr.map(normalizeOne).map(obj => ({ json: obj })); +return results.length ? results : [{ json: null }]; +``` + +--- + +### 2. Code6 — генерация промптов для RAG + +```javascript +// n8n Code node: Генерация prompt'а под конкретный тип +// ВХОД: { type: 'user'|'project'|'offenders', data: {...} } + +const type = $json.type; +const data = $json.data; + +const code1Data = (() => { + try { + return $('Code1').first().json || {}; + } catch(_) { + return {}; + } +})(); + +const aiAnalysis = code1Data.ai_analysis || {}; +const problemDescription = code1Data.problem_description || ''; +const wizardPlan = code1Data.wizard_plan || {}; +const caseType = wizardPlan.case_type || code1Data.case?.type_code || 'consumer'; + +let schema = ''; +let searchHints = ''; +let contextInfo = ''; + +contextInfo = ` +КОНТЕКСТ ДЕЛА: +- Тип: ${caseType} +- Проблема: ${aiAnalysis.problem || 'не указана'} +- Краткие факты: ${aiAnalysis.facts_short || 'не указаны'} +`; + +if (type === 'user') { + schema = `{ + "user": { + "firstname": string|null, + "secondname": string|null, + "lastname": string|null, + "mobile": string|null, + "email": string|null, + "tgid": number|null, + "birthday": "YYYY-MM-DD"|null, + "birthplace": string|null, + "mailingstreet": string|null, + "inn": string|null (12 цифр для физлица) + } + }`; + + searchHints = `Ищи данные ПОКУПАТЕЛЯ/ЗАКАЗЧИКА: +- ФИО: после "Покупатель:", "Заказчик:", "Потребитель:" +- Адрес: "адрес регистрации", "адрес проживания", "место жительства" +- ИНН физлица = 12 цифр +- Телефон: в реквизитах, после "тел:", "моб:" +- Email: в реквизитах`; + +} else if (type === 'project') { + schema = `{ + "project": { + "category": string|null (тема обращения), + "direction": string|null, + "agrprice": number|null (сумма в рублях, только цифры!), + "subject": string|null (предмет договора - что купили/заказали), + "agrdate": "YYYY-MM-DD"|null (дата заключения договора), + "startdate": "YYYY-MM-DD"|null (дата начала услуги/поездки), + "finishdate": "YYYY-MM-DD"|null (дата окончания), + "country": string|null (страна для турпутёвок), + "hotel": string|null (название отеля), + "transport": "да"|"нет"|null (включён ли трансфер), + "insurance": "да"|"нет"|null (включена ли страховка), + "description": string|null (краткое описание сделки) + } + }`; + + searchHints = `Ищи данные ДОГОВОРА/СДЕЛКИ: +- Сумма: "Цена договора", "Стоимость", "Итого к оплате", "Сумма заказа" +- Дата: "Дата заключения", "Договор № ... от ...", "Заказ от ..." +- Предмет: что именно купили или заказали (товар, услуга, тур) +- Для туров: страна, отель, даты заезда/выезда`; + +} else if (type === 'offenders') { + schema = `{ + "offenders": [ + { + "role": "seller"|"service_provider"|"tour_agent"|"tour_operator"|"delivery"|"installer"|"intermediary"|null, + "accountname": string|null (название организации или ФИО ИП), + "address": string|null (юридический адрес), + "email": string|null, + "website": string|null, + "phone": string|null, + "inn": string|null (10 цифр для юрлица, 12 для ИП), + "ogrn": string|null (13 цифр ОГРН или 15 цифр ОГРНИП) + } + ] + }`; + + searchHints = `Ищи данные ВСЕХ КОНТРАГЕНТОВ (может быть несколько!): + +ГДЕ ИСКАТЬ: +- "Продавец:", "Исполнитель:", "Поставщик:" +- После ООО, ИП, ЗАО, ОАО, ПАО, АО +- В реквизитах договора, в шапке чека + +РЕКВИЗИТЫ: +- ИНН юрлица = 10 цифр, ИП = 12 цифр +- ОГРН = 13 цифр, ОГРНИП = 15 цифр + +РОЛИ (определи по контексту): +- seller — продавец товара (магазин, салон) +- service_provider — исполнитель услуги +- tour_agent — турагент (кто продал путёвку) +- tour_operator — туроператор (кто организует тур, указан в договоре отдельно) +- delivery — служба доставки +- installer — сборщик/установщик +- intermediary — посредник, маркетплейс + +ВАЖНО: Если в документах несколько организаций — добавь всех!`; +} + +const filledCount = Object.values(data || {}).filter(v => v !== null && v !== undefined && v !== '').length; +const totalCount = Object.keys(data || {}).length; + +return [{ + json: { + systemMessage: `Ты — юридический помощник-экстрактор. У тебя есть инструмент vectorStore для поиска по документам. + +${contextInfo} + +${searchHints} + +ПРАВИЛА: +1. Ищи только поля из схемы ниже +2. Возвращай строго JSON в указанном формате +3. Если данные не найдены — ставь null +4. НЕ ПРИДУМЫВАЙ данные! +5. Дозаполняй только пустые/null поля`, + + userMessage: `Текущие данные (заполнено ${filledCount} из ${totalCount}, дозаполни остальное): +${JSON.stringify(data, null, 2)} + +Схема для ответа: +${schema}`, + + _meta: { + type, + filledCount, + totalCount, + caseType + } + } +}]; +``` + +--- + +### 3. prepare_rag_items — создание 3 items для RAG (НУЖНО ДОБАВИТЬ!) + +```javascript +// Code node: prepare_rag_items +// Создаёт 3 items для RAG: user, project, offenders +// Вставить между Code1 и Loop Over Items + +const src = $('Code1').first().json; + +// USER — из уже собранных данных Code1 +const userData = src.user || {}; + +// PROJECT — пока пустой, RAG заполнит +const projectData = { + category: null, + direction: null, + agrprice: null, + subject: null, + agrdate: null, + startdate: null, + finishdate: null, + country: null, + hotel: null, + transport: null, + insurance: null, + description: src.problem_description || null, +}; + +// OFFENDERS — пока пустой массив, RAG найдёт +const offendersData = []; + +// Выдаём 3 items для Loop Over Items +return [ + { json: { type: 'user', data: userData } }, + { json: { type: 'project', data: projectData } }, + { json: { type: 'offenders', data: offendersData } } +]; +``` + +--- + +## ✅ Результат работы workflow + +Тестовый запуск на claim `509872e2-9666-4c5e-8ab7-2304dd6a5d18`: + +### USER — полностью заполнен +- firstname: Федор +- secondname: Владимирович +- lastname: Коробков +- mobile: 79262306381 +- email: help@clientright.ru +- tgid: 295410106 +- birthday: 1981-09-18 +- birthplace: Москва +- mailingstreet: МО, г. Балашиха, мкр. Железнодорожный, ул. Советская, д.20, кв. 52 + +### PROJECT — основное заполнено +- category: задержка ремонта/недоставка комплектующих и отказ в оказании услуги сборки +- agrprice: **89620** (сумма договора) +- subject: кровать-подиум Hemwood Base 180х200 и тумбы к ней +- agrdate: **2025-08-09** (дата договора) +- startdate: **2025-08-16** (дата доставки) +- description: полное описание проблемы + +### OFFENDERS — найдено 2 контрагента! + +**1. Продавец (seller):** +- accountname: ИП Хациев Зелимхан Зелимханович +- inn: 201471261963 (12 цифр — ИП ✅) +- ogrn: 315774600000123 (15 цифр — ОГРНИП ✅) +- website: raiton.ru + +**2. Исполнитель услуг (service_provider):** +- accountname: АО «ОРМАТЕК» +- inn: 7724890784 (10 цифр — юрлицо ✅) +- email: kassa@ormatek.com + +--- + +## 📋 TODO (следующие шаги) + +1. [ ] Добавить ноду `prepare_rag_items` между Code1 и Loop Over Items +2. [ ] Добавить постобработку данных (валидация, исправление ошибок AI) +3. [ ] Сохранение результата в Redis для формы +4. [ ] Подключить к генерации формы заявления + +--- + +## 📁 Связанные файлы + +- `ticket_form/docs/SQL_CLAIMSAVE_FINAL_FIXED_NEW_FLOW_WITH_UPLOADED_FIXED.sql` — SQL для сохранения документов +- `ticket_form/frontend/src/components/form/generateConfirmationFormHTML.ts` — шаблон формы заявления + diff --git a/ticket_form/docs/n8n_nodes/README_SETUP.md b/ticket_form/docs/n8n_nodes/README_SETUP.md new file mode 100644 index 00000000..4a109c09 --- /dev/null +++ b/ticket_form/docs/n8n_nodes/README_SETUP.md @@ -0,0 +1,99 @@ +# Настройка OCR Status Tracking в n8n + +## Шаги для добавления нод в workflow `fnSo3FTTbQcMjwt3` + +### 1. Открой workflow в n8n +https://n8n.clientright.pro/workflow/fnSo3FTTbQcMjwt3 + +### 2. Добавь ноды в следующем порядке: + +#### Нода 1: `update_ocr_status` (PostgreSQL) +**Расположение:** После `Postgres PGVector Store1` +**Позиция:** [3850, 1664] + +**SQL запрос:** +```sql +UPDATE clpr_claim_documents +SET + ocr_status = 'ready', + ocr_processed_at = NOW() +WHERE id = '{{ $('files').item.json.claim_document_id }}'::uuid +RETURNING + id AS doc_id, + claim_id, + ocr_status, + (SELECT COUNT(*) FROM clpr_claim_documents WHERE claim_id = clpr_claim_documents.claim_id) AS total_docs, + (SELECT COUNT(*) FROM clpr_claim_documents WHERE claim_id = clpr_claim_documents.claim_id AND ocr_status = 'ready') AS ready_docs; +``` + +**Credentials:** `timeweb_bd` (Postgres account 2) + +--- + +#### Нода 2: `redis_incr_ready` (Redis) +**Расположение:** После `update_ocr_status` +**Позиция:** [4100, 1664] + +**Параметры:** +- Operation: `incr` +- Key: `claim:ocr:ready:{{ $json.claim_id }}` + +**Credentials:** `Redis account` + +--- + +#### Нода 3: `check_all_ready` (IF) +**Расположение:** После `redis_incr_ready` +**Позиция:** [4350, 1664] + +**Условие:** +``` +{{ $('update_ocr_status').item.json.total_docs }} == {{ $('update_ocr_status').item.json.ready_docs }} +``` + +--- + +#### Нода 4: `publish_docs_ready` (Redis) +**Расположение:** TRUE выход из `check_all_ready` +**Позиция:** [4600, 1550] + +**Параметры:** +- Operation: `publish` +- Channel: `clpr:claim:docs_ready` +- Message: +```javascript +{{ JSON.stringify({ + claim_id: $('update_ocr_status').item.json.claim_id, + total_docs: $('update_ocr_status').item.json.total_docs, + ready_docs: $('update_ocr_status').item.json.ready_docs, + timestamp: new Date().toISOString() +}) }} +``` + +**Credentials:** `Redis account` + +--- + +### 3. Соединения (Connections) + +``` +Postgres PGVector Store1 → update_ocr_status +update_ocr_status → redis_incr_ready +redis_incr_ready → check_all_ready +check_all_ready (TRUE) → publish_docs_ready +check_all_ready (FALSE) → (конец) +``` + +### 4. Сохрани и активируй workflow + +--- + +## Проверка + +После добавления нод, при обработке документа: +1. Статус в `clpr_claim_documents.ocr_status` будет меняться на `'ready'` +2. Счётчик в Redis `claim:ocr:ready:{claim_id}` будет инкрементиться +3. Когда все документы готовы, событие `clpr:claim:docs_ready` будет опубликовано в Redis + + + diff --git a/ticket_form/docs/n8n_nodes/check_all_ready.json b/ticket_form/docs/n8n_nodes/check_all_ready.json new file mode 100644 index 00000000..15104925 --- /dev/null +++ b/ticket_form/docs/n8n_nodes/check_all_ready.json @@ -0,0 +1,32 @@ +{ + "name": "check_all_ready", + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "position": [4350, 1664], + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict", + "version": 2 + }, + "conditions": [ + { + "id": "ocr-ready-check-001", + "leftValue": "={{ $('update_ocr_status').item.json.total_docs }}", + "rightValue": "={{ $('update_ocr_status').item.json.ready_docs }}", + "operator": { + "type": "number", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "options": {} + } +} + + + diff --git a/ticket_form/docs/n8n_nodes/publish_docs_ready.json b/ticket_form/docs/n8n_nodes/publish_docs_ready.json new file mode 100644 index 00000000..a0949262 --- /dev/null +++ b/ticket_form/docs/n8n_nodes/publish_docs_ready.json @@ -0,0 +1,20 @@ +{ + "name": "publish_docs_ready", + "type": "n8n-nodes-base.redis", + "typeVersion": 1, + "position": [4600, 1550], + "parameters": { + "operation": "publish", + "channel": "clpr:claim:docs_ready", + "messageData": "={{ JSON.stringify({ claim_id: $('update_ocr_status').item.json.claim_id, total_docs: $('update_ocr_status').item.json.total_docs, ready_docs: $('update_ocr_status').item.json.ready_docs, timestamp: new Date().toISOString() }) }}" + }, + "credentials": { + "redis": { + "id": "F2IkIEYT9O7UTtz9", + "name": "Redis account" + } + } +} + + + diff --git a/ticket_form/docs/n8n_nodes/redis_incr_ready.json b/ticket_form/docs/n8n_nodes/redis_incr_ready.json new file mode 100644 index 00000000..39747e7b --- /dev/null +++ b/ticket_form/docs/n8n_nodes/redis_incr_ready.json @@ -0,0 +1,19 @@ +{ + "name": "redis_incr_ready", + "type": "n8n-nodes-base.redis", + "typeVersion": 1, + "position": [4100, 1664], + "parameters": { + "operation": "incr", + "key": "=claim:ocr:ready:{{ $json.claim_id }}" + }, + "credentials": { + "redis": { + "id": "F2IkIEYT9O7UTtz9", + "name": "Redis account" + } + } +} + + + diff --git a/ticket_form/docs/n8n_nodes/update_ocr_error.json b/ticket_form/docs/n8n_nodes/update_ocr_error.json new file mode 100644 index 00000000..c862f8a7 --- /dev/null +++ b/ticket_form/docs/n8n_nodes/update_ocr_error.json @@ -0,0 +1,21 @@ +{ + "name": "update_ocr_error", + "type": "n8n-nodes-base.postgres", + "typeVersion": 2.6, + "position": [3850, 1850], + "parameters": { + "operation": "executeQuery", + "query": "-- Обновляем статус OCR при ошибке\nUPDATE clpr_claim_documents\nSET \n ocr_status = 'error',\n ocr_error = '{{ $json.error || \"OCR processing failed\" }}',\n ocr_processed_at = NOW()\nWHERE id = '{{ $('files').item.json.claim_document_id }}'::uuid\nRETURNING id, claim_id, ocr_status, ocr_error;", + "options": {} + }, + "credentials": { + "postgres": { + "id": "sGJ0fJhU8rz88w3k", + "name": "timeweb_bd" + } + }, + "onError": "continueRegularOutput" +} + + + diff --git a/ticket_form/docs/n8n_nodes/update_ocr_status.json b/ticket_form/docs/n8n_nodes/update_ocr_status.json new file mode 100644 index 00000000..1d8fdead --- /dev/null +++ b/ticket_form/docs/n8n_nodes/update_ocr_status.json @@ -0,0 +1,20 @@ +{ + "name": "update_ocr_status", + "type": "n8n-nodes-base.postgres", + "typeVersion": 2.6, + "position": [3850, 1664], + "parameters": { + "operation": "executeQuery", + "query": "-- Обновляем статус OCR для документа и возвращаем счётчики\nUPDATE clpr_claim_documents\nSET \n ocr_status = 'ready',\n ocr_processed_at = NOW()\nWHERE id = '{{ $('files').item.json.claim_document_id }}'::uuid\nRETURNING \n id AS doc_id,\n claim_id,\n ocr_status,\n (SELECT COUNT(*) FROM clpr_claim_documents WHERE claim_id = clpr_claim_documents.claim_id) AS total_docs,\n (SELECT COUNT(*) FROM clpr_claim_documents WHERE claim_id = clpr_claim_documents.claim_id AND ocr_status = 'ready') AS ready_docs;", + "options": {} + }, + "credentials": { + "postgres": { + "id": "sGJ0fJhU8rz88w3k", + "name": "timeweb_bd" + } + } +} + + +