Files
aiform_prod/docs/FIXED_SQL_QUERY.md
AI Assistant 4c8fda5f55 Добавлено логирование для отладки черновиков
- Добавлены логи в frontend (ClaimForm.tsx) для отслеживания unified_id и запросов к API
- Добавлены логи в backend (claims.py) для отладки SQL запросов
- Создан лог сессии с описанием проблемы и текущего состояния
- Проблема: API возвращает 0 черновиков, хотя в БД есть данные
2025-11-19 18:46:48 +03:00

10 KiB
Raw Blame History

Исправленный 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 запрос

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

Параметры запроса

// В 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) более надежный и правильный.