# Исправленный SQL запрос для сохранения заявки ## Проблема Оригинальный SQL запрос использует `$2::uuid`, но передается строка `"CLM-2025-11-18-GEQ3KL"`, что вызывает ошибку: ``` invalid input syntax for type uuid: "CLM-2025-11-18-GEQ3KL" ``` ## Решение Изменить SQL запрос так, чтобы он: 1. Принимал `claim_id` как строку (VARCHAR) 2. Искал запись в `clpr_claims` по `payload->>'claim_id'` или создавал новую 3. Использовал найденный UUID для дальнейших операций ## Исправленный SQL запрос ```sql WITH partial AS ( SELECT $1::jsonb AS p, $2::text AS claim_id_str ), -- Сначала находим существующую запись или создаем новую claim_lookup AS ( SELECT COALESCE( (SELECT id FROM clpr_claims WHERE payload->>'claim_id' = partial.claim_id_str LIMIT 1), gen_random_uuid() ) AS claim_uuid FROM partial ), -- Если записи нет, создаем её claim_created AS ( INSERT INTO clpr_claims ( id, session_token, channel, type_code, status_code, payload, created_at, updated_at, expires_at ) SELECT claim_lookup.claim_uuid, COALESCE(partial.p->>'session_id', 'sess-' || gen_random_uuid()::text), 'web_form', COALESCE(partial.p->>'type_code', 'consumer'), 'draft', jsonb_build_object( 'claim_id', partial.claim_id_str, 'answers', CASE -- В корне WHEN partial.p->>'wizard_answers' IS NOT NULL THEN (partial.p->>'wizard_answers')::jsonb -- В edit_fields_raw.body WHEN partial.p->'edit_fields_raw'->'body'->>'wizard_answers' IS NOT NULL THEN (partial.p->'edit_fields_raw'->'body'->>'wizard_answers')::jsonb -- В edit_fields_parsed.body WHEN partial.p->'edit_fields_parsed'->'body'->>'wizard_answers' IS NOT NULL THEN (partial.p->'edit_fields_parsed'->'body'->>'wizard_answers')::jsonb -- Если уже объект WHEN partial.p->'wizard_answers' IS NOT NULL AND jsonb_typeof(partial.p->'wizard_answers') = 'object' THEN partial.p->'wizard_answers' ELSE '{}'::jsonb END, 'documents_meta', COALESCE(partial.p->'documents_meta', '[]'::jsonb), 'wizard_plan', CASE -- В корне WHEN partial.p->>'wizard_plan' IS NOT NULL THEN (partial.p->>'wizard_plan')::jsonb -- В edit_fields_raw.body WHEN partial.p->'edit_fields_raw'->'body'->>'wizard_plan' IS NOT NULL THEN (partial.p->'edit_fields_raw'->'body'->>'wizard_plan')::jsonb -- В edit_fields_parsed.body WHEN partial.p->'edit_fields_parsed'->'body'->>'wizard_plan' IS NOT NULL THEN (partial.p->'edit_fields_parsed'->'body'->>'wizard_plan')::jsonb -- Если уже объект WHEN partial.p->'wizard_plan' IS NOT NULL AND jsonb_typeof(partial.p->'wizard_plan') = 'object' THEN partial.p->'wizard_plan' ELSE NULL END ), now(), now(), now() + interval '14 days' FROM partial, claim_lookup WHERE NOT EXISTS ( SELECT 1 FROM clpr_claims WHERE id = claim_lookup.claim_uuid ) ON CONFLICT (id) DO NOTHING RETURNING id ), -- Получаем финальный UUID (из существующей записи или только что созданной) claim_final AS ( SELECT CASE WHEN EXISTS (SELECT 1 FROM claim_created) THEN (SELECT id FROM claim_created LIMIT 1) ELSE claim_lookup.claim_uuid END AS claim_uuid FROM claim_lookup ), inserted_docs AS ( INSERT INTO clpr_claim_documents (claim_id, field_name, file_id, uploaded_at, file_name, original_file_name) SELECT claim_final.claim_uuid::text AS claim_id, doc.field_name, doc.file_id, (doc.uploaded_at)::timestamptz AS uploaded_at, doc.file_name, doc.original_file_name FROM partial, claim_final CROSS JOIN LATERAL jsonb_to_recordset( COALESCE(partial.p->'documents_meta','[]'::jsonb) ) AS doc( field_name text, file_id text, file_name text, original_file_name text, uploaded_at text ) ON CONFLICT (claim_id, field_name) DO UPDATE SET file_id = EXCLUDED.file_id, uploaded_at = EXCLUDED.uploaded_at, file_name = EXCLUDED.file_name, original_file_name = EXCLUDED.original_file_name RETURNING id, claim_id, field_name, file_id ), existing AS ( SELECT c.id, c.payload FROM clpr_claims c, claim_final WHERE c.id = claim_final.claim_uuid FOR UPDATE ), old AS ( SELECT COALESCE( (SELECT payload FROM existing LIMIT 1), '{}'::jsonb ) AS old_payload FROM claim_final ), -- Парсим wizard_answers из строки в JSON объект -- Ищем в разных местах: корень, edit_fields_raw.body, edit_fields_parsed.body wizard_answers_parsed AS ( SELECT CASE -- В корне payload_partial_json WHEN partial.p->>'wizard_answers' IS NOT NULL THEN (partial.p->>'wizard_answers')::jsonb -- В edit_fields_raw.body.wizard_answers WHEN partial.p->'edit_fields_raw'->'body'->>'wizard_answers' IS NOT NULL THEN (partial.p->'edit_fields_raw'->'body'->>'wizard_answers')::jsonb -- В edit_fields_parsed.body.wizard_answers WHEN partial.p->'edit_fields_parsed'->'body'->>'wizard_answers' IS NOT NULL THEN (partial.p->'edit_fields_parsed'->'body'->>'wizard_answers')::jsonb -- Если уже объект (не строка) WHEN partial.p->'wizard_answers' IS NOT NULL AND jsonb_typeof(partial.p->'wizard_answers') = 'object' THEN partial.p->'wizard_answers' ELSE '{}'::jsonb END AS answers FROM partial ), -- Парсим wizard_plan из строки в JSON объект -- Ищем в разных местах: корень, edit_fields_raw.body, edit_fields_parsed.body wizard_plan_parsed AS ( SELECT CASE -- В корне payload_partial_json WHEN partial.p->>'wizard_plan' IS NOT NULL THEN (partial.p->>'wizard_plan')::jsonb -- В edit_fields_raw.body.wizard_plan WHEN partial.p->'edit_fields_raw'->'body'->>'wizard_plan' IS NOT NULL THEN (partial.p->'edit_fields_raw'->'body'->>'wizard_plan')::jsonb -- В edit_fields_parsed.body.wizard_plan WHEN partial.p->'edit_fields_parsed'->'body'->>'wizard_plan' IS NOT NULL THEN (partial.p->'edit_fields_parsed'->'body'->>'wizard_plan')::jsonb -- Если уже объект (не строка) WHEN partial.p->'wizard_plan' IS NOT NULL AND jsonb_typeof(partial.p->'wizard_plan') = 'object' THEN partial.p->'wizard_plan' ELSE NULL END AS wizard_plan FROM partial ), -- Объединяем documents_meta без дублирования (используем новый, если есть) docs_merged AS ( SELECT COALESCE( NULLIF(partial.p->'documents_meta', 'null'::jsonb), old.old_payload->'documents_meta', '[]'::jsonb ) AS documents_meta FROM old, partial ), -- Формируем чистый payload (без лишних полей) clean_payload AS ( SELECT jsonb_build_object( 'claim_id', partial.claim_id_str, 'answers', (SELECT answers FROM wizard_answers_parsed LIMIT 1), 'documents_meta', (SELECT documents_meta FROM docs_merged LIMIT 1), 'wizard_plan', (SELECT wizard_plan FROM wizard_plan_parsed LIMIT 1) ) AS clean FROM partial ), upd AS ( UPDATE clpr_claims c SET payload = ( -- Сохраняем только нужные поля из старого payload COALESCE(old.old_payload, '{}'::jsonb) - 'answers' - 'documents_meta' - 'wizard_plan' - 'wizard_answers' - 'form_data' - 'edit_fields_raw' - 'edit_fields_parsed' -- Добавляем чистый payload || (SELECT clean FROM clean_payload LIMIT 1) ), status_code = CASE WHEN ( (SELECT answers->>'docs_exist' FROM wizard_answers_parsed LIMIT 1) = 'true' ) THEN 'in_work' ELSE COALESCE(c.status_code, 'draft') END, updated_at = now(), expires_at = now() + interval '14 days' FROM partial, old, claim_final, clean_payload WHERE c.id = claim_final.claim_uuid RETURNING c.id, c.status_code, c.payload ) SELECT (SELECT jsonb_build_object( 'claim_id', u.id::text, 'claim_id_str', (u.payload->>'claim_id'), 'status_code', u.status_code, 'payload', u.payload ) FROM upd u) AS claim, (SELECT jsonb_agg(jsonb_build_object( 'id', id, 'field_name', field_name, 'file_id', file_id )) FROM inserted_docs) AS documents; ``` ## Изменения 1. **`claim_id_str` вместо `uuid`**: `$2::text AS claim_id_str` вместо `$2::uuid AS cid` 2. **`claim_lookup` CTE**: Находит существующую запись по `payload->>'claim_id'` или генерирует новый UUID 3. **`claim_created` CTE**: Создает новую запись, если её нет 4. **Использование `claim_uuid`**: Во всех местах используется UUID из `claim_lookup`, а не строка 5. **`claim_id` в `clpr_claim_documents`**: Преобразуется в строку `claim_uuid::text`, т.к. в таблице `claim_id` имеет тип `character varying` ## Параметры запроса ```javascript // В n8n PostgreSQL Node Parameters: $1 = JSONB с данными (payload_partial_json) $2 = TEXT с claim_id ("CLM-2025-11-18-GEQ3KL") ``` ## Альтернативное решение (если не хотите менять SQL) Если не хотите менять SQL запрос, можно изменить логику в n8n: 1. **Перед SQL запросом** добавить Code Node, который: - Находит запись в `clpr_claims` по `payload->>'claim_id'` - Если найдена - использует её `id` (UUID) - Если не найдена - создает новую запись и возвращает её `id` 2. **Передавать UUID** вместо строки `claim_id` в SQL запрос Но первый вариант (изменение SQL) более надежный и правильный.