Files
aiform_dev/docs/SQL_CLAIMSAVE_UPSERT_SIMPLE.sql
AI Assistant 40ad46c026 fix: Parse wizard_plan from edit_fields_parsed structure
Problem:
- wizard_plan comes in nested structure: edit_fields_parsed.wizard_plan_parsed
- Old SQL looked for wizard_plan at root level (partial.p->'wizard_plan')
- Result: wizard_plan was always NULL even though it existed in payload

Solution:
- Updated wizard_plan_parsed CTE to check multiple locations:
  1. edit_fields_parsed.wizard_plan_parsed (already parsed object) 
  2. edit_fields_raw.body.wizard_plan (escaped JSON string)
  3. Root level wizard_plan (backward compatibility)
  4. Database fallback (if not in payload)

- Updated wizard_answers_parsed CTE:
  1. edit_fields_raw.body.wizard_answers (string) 
  2. edit_fields_parsed.body.wizard_answers (string)
  3. Root level (backward compatibility)

- Updated session_id, unified_id, contact_id, phone extraction:
  COALESCE() checks edit_fields_parsed.body and edit_fields_raw.body

Input structure from n8n workflow form_get:
{
  "payload_partial_json": {
    "edit_fields_parsed": {
      "wizard_plan_parsed": {...},  ← TARGET
      "body": {
        "session_id": "sess_xxx",  ← ALSO HERE
        "unified_id": "usr_xxx",
        "wizard_answers": "{...}"
      }
    }
  }
}

Now: wizard_plan preserves correctly when uploading files 
2025-11-21 16:10:57 +03:00

402 lines
14 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- Упрощённый UPSERT для сохранения claim с известным claim_id
-- Используется в n8n workflow: form_get (нода claimsave)
-- Дата: 2025-11-21 (обновлено: сохранение существующих полей)
-- Описание: Простой INSERT/UPDATE для claim, т.к. claim_id уже известен
-- ВАЖНО: Сохраняет wizard_plan и другие поля из БД, если не пришли новые
-- Входные параметры:
-- $1: payload_partial_json (jsonb) - данные формы с wizard_answers, wizard_plan, documents_meta
-- $2: claim_id (text) - UUID заявки
WITH partial AS (
SELECT
$1::jsonb AS p,
$2::text AS claim_id_str
),
-- Получаем существующий payload из БД (если запись есть)
existing_claim AS (
SELECT
id,
payload
FROM clpr_claims
WHERE id = (SELECT claim_id_str::uuid FROM partial)
LIMIT 1
),
-- Парсим wizard_answers
wizard_answers_parsed AS (
SELECT
CASE
-- Из 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
THEN (partial.p->>'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 (или берём из существующей записи)
wizard_plan_parsed AS (
SELECT
CASE
-- Сначала пытаемся взять из edit_fields_parsed.wizard_plan_parsed (уже объект)
WHEN partial.p->'edit_fields_parsed'->'wizard_plan_parsed' IS NOT NULL
AND jsonb_typeof(partial.p->'edit_fields_parsed'->'wizard_plan_parsed') = 'object'
THEN partial.p->'edit_fields_parsed'->'wizard_plan_parsed'
-- Или из корня как строка
WHEN partial.p->>'wizard_plan' IS NOT NULL
THEN (partial.p->>'wizard_plan')::jsonb
-- Или из корня как объект
WHEN partial.p->'wizard_plan' IS NOT NULL
AND jsonb_typeof(partial.p->'wizard_plan') = 'object'
THEN partial.p->'wizard_plan'
-- Или из 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
-- Если нет в payload - берём из существующей записи в БД
WHEN EXISTS (SELECT 1 FROM existing_claim WHERE payload->'wizard_plan' IS NOT NULL)
THEN (SELECT payload->'wizard_plan' FROM existing_claim)
ELSE NULL
END AS wizard_plan
FROM partial
),
-- Парсим answers_prefill (или берём из БД)
answers_prefill_parsed AS (
SELECT
CASE
WHEN partial.p->'answers_prefill' IS NOT NULL
AND jsonb_typeof(partial.p->'answers_prefill') = 'array'
THEN partial.p->'answers_prefill'
WHEN EXISTS (SELECT 1 FROM existing_claim WHERE payload->'answers_prefill' IS NOT NULL)
THEN (SELECT payload->'answers_prefill' FROM existing_claim)
ELSE '[]'::jsonb
END AS answers_prefill
FROM partial
),
-- Парсим coverage_report (или берём из БД)
coverage_report_parsed AS (
SELECT
CASE
WHEN partial.p->'coverage_report' IS NOT NULL
AND jsonb_typeof(partial.p->'coverage_report') = 'object'
THEN partial.p->'coverage_report'
WHEN EXISTS (SELECT 1 FROM existing_claim WHERE payload->'coverage_report' IS NOT NULL)
THEN (SELECT payload->'coverage_report' FROM existing_claim)
ELSE NULL
END AS coverage_report
FROM partial
),
-- Парсим ai_agent1_facts (или берём из БД)
ai_agent1_facts_parsed AS (
SELECT
CASE
WHEN partial.p->'ai_agent1_facts' IS NOT NULL
AND jsonb_typeof(partial.p->'ai_agent1_facts') = 'object'
THEN partial.p->'ai_agent1_facts'
WHEN EXISTS (SELECT 1 FROM existing_claim WHERE payload->'ai_agent1_facts' IS NOT NULL)
THEN (SELECT payload->'ai_agent1_facts' FROM existing_claim)
ELSE NULL
END AS ai_agent1_facts
FROM partial
),
-- Парсим ai_agent13_rag (или берём из БД)
ai_agent13_rag_parsed AS (
SELECT
CASE
WHEN partial.p->'ai_agent13_rag' IS NOT NULL
THEN partial.p->'ai_agent13_rag'
WHEN partial.p->>'ai_agent13_rag' IS NOT NULL
THEN to_jsonb(partial.p->>'ai_agent13_rag')
WHEN EXISTS (SELECT 1 FROM existing_claim WHERE payload->'ai_agent13_rag' IS NOT NULL)
THEN (SELECT payload->'ai_agent13_rag' FROM existing_claim)
ELSE NULL
END AS ai_agent13_rag
FROM partial
),
-- Парсим problem_description (или берём из БД)
problem_description_parsed AS (
SELECT
CASE
WHEN partial.p->>'problem_description' IS NOT NULL
THEN partial.p->>'problem_description'
WHEN EXISTS (SELECT 1 FROM existing_claim WHERE payload->>'problem_description' IS NOT NULL)
THEN (SELECT payload->>'problem_description' FROM existing_claim)
ELSE NULL
END AS problem_description
FROM partial
),
-- UPSERT claim
claim_upsert AS (
INSERT INTO clpr_claims (
id,
session_token,
unified_id,
contact_id,
phone,
channel,
type_code,
status_code,
payload,
created_at,
updated_at,
expires_at
)
SELECT
partial.claim_id_str::uuid,
COALESCE(
partial.p->>'session_id',
partial.p->'edit_fields_parsed'->'body'->>'session_id',
partial.p->'edit_fields_raw'->'body'->>'session_id',
'sess-unknown'
),
COALESCE(
partial.p->>'unified_id',
partial.p->'edit_fields_parsed'->'body'->>'unified_id',
partial.p->'edit_fields_raw'->'body'->>'unified_id'
),
COALESCE(
partial.p->>'contact_id',
partial.p->'edit_fields_parsed'->'body'->>'contact_id',
partial.p->'edit_fields_raw'->'body'->>'contact_id'
),
COALESCE(
partial.p->>'phone',
partial.p->'edit_fields_parsed'->'body'->>'phone',
partial.p->'edit_fields_raw'->'body'->>'phone'
),
'web_form',
COALESCE(partial.p->>'type_code', 'consumer'),
CASE
WHEN (SELECT answers->>'docs_exist' FROM wizard_answers_parsed) = 'true'
THEN 'in_work'
ELSE 'draft'
END,
jsonb_build_object(
'claim_id', partial.claim_id_str,
'problem_description', (SELECT problem_description FROM problem_description_parsed),
'answers', (SELECT answers FROM wizard_answers_parsed),
'documents_meta', COALESCE(partial.p->'documents_meta', '[]'::jsonb),
'wizard_plan', (SELECT wizard_plan FROM wizard_plan_parsed),
'answers_prefill', (SELECT answers_prefill FROM answers_prefill_parsed),
'coverage_report', (SELECT coverage_report FROM coverage_report_parsed),
'ai_agent1_facts', (SELECT ai_agent1_facts FROM ai_agent1_facts_parsed),
'ai_agent13_rag', (SELECT ai_agent13_rag FROM ai_agent13_rag_parsed),
'phone', COALESCE(partial.p->>'phone', (SELECT payload->>'phone' FROM existing_claim)),
'email', COALESCE(partial.p->>'email', (SELECT payload->>'email' FROM existing_claim))
),
COALESCE(
(SELECT created_at FROM clpr_claims WHERE id = partial.claim_id_str::uuid),
now()
),
now(),
now() + interval '14 days'
FROM partial
ON CONFLICT (id) DO UPDATE SET
session_token = EXCLUDED.session_token,
unified_id = COALESCE(EXCLUDED.unified_id, clpr_claims.unified_id),
contact_id = COALESCE(EXCLUDED.contact_id, clpr_claims.contact_id),
phone = COALESCE(EXCLUDED.phone, clpr_claims.phone),
status_code = EXCLUDED.status_code,
payload = EXCLUDED.payload,
updated_at = now(),
expires_at = now() + interval '14 days'
RETURNING id, status_code, payload, unified_id, contact_id, phone, session_token
),
-- UPSERT documents (если есть)
docs_upsert AS (
INSERT INTO clpr_claim_documents (
claim_id,
field_name,
file_id,
uploaded_at,
file_name,
original_file_name
)
SELECT
partial.claim_id_str AS claim_id,
doc.field_name,
doc.file_id,
COALESCE((doc.uploaded_at)::timestamptz, now()),
doc.file_name,
doc.original_file_name
FROM partial
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
)
WHERE partial.p->'documents_meta' IS NOT NULL
AND jsonb_array_length(partial.p->'documents_meta') > 0
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, file_name, original_file_name
)
-- Возвращаем результат
SELECT
(SELECT jsonb_build_object(
'claim_id', cu.id::text,
'claim_id_str', (cu.payload->>'claim_id'),
'status_code', cu.status_code,
'unified_id', cu.unified_id,
'contact_id', cu.contact_id,
'phone', cu.phone,
'session_token', cu.session_token,
'payload', cu.payload
) FROM claim_upsert cu) AS claim,
(SELECT jsonb_agg(jsonb_build_object(
'id', id,
'field_name', field_name,
'file_id', file_id,
'file_name', file_name,
'original_file_name', original_file_name
)) FROM docs_upsert) AS documents;
/*
============================================================================
КЛЮЧЕВЫЕ ИЗМЕНЕНИЯ (2025-11-21):
============================================================================
1. Добавлена CTE "existing_claim" - читает существующий payload из БД
2. Все парсеры (wizard_plan, answers_prefill, coverage_report и т.д.)
теперь проверяют БД, если поле не пришло в payload_partial_json
3. Это критично для workflow form_get, где wizard_plan создаётся на Step 2,
а файлы загружаются на Step 3 (без повторной отправки wizard_plan)
============================================================================
ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:
============================================================================
1. Первое сохранение (после Step 2 - описание проблемы):
SELECT * FROM ... WHERE ... = (
'{
"session_id": "sess_xxx",
"unified_id": "usr_xxx",
"contact_id": "12345",
"phone": "79262306381",
"problem_description": "Не вернули деньги...",
"wizard_plan": {...полный wizard_plan...},
"ai_agent1_facts": {...},
"ai_agent13_rag": "...",
"answers_prefill": [...],
"coverage_report": {...}
}'::jsonb,
'uuid-here'::text
);
Результат: Создаётся claim с wizard_plan ✅
---
2. Обновление с файлами (Step 3 - загрузка документов):
SELECT * FROM ... WHERE ... = (
'{
"session_id": "sess_xxx",
"unified_id": "usr_xxx",
"wizard_answers": {"q1": "answer1"},
"documents_meta": [
{
"field_name": "uploads[0][0]",
"file_id": "clientright/0/file.pdf",
"file_name": "file.pdf",
"original_file_name": "original.pdf",
"uploaded_at": "2025-11-21T12:00:00Z"
}
]
}'::jsonb,
'СУЩЕСТВУЮЩИЙ-uuid'::text
);
Результат:
- Обновляется answers ✅
- Добавляются documents_meta ✅
- wizard_plan СОХРАНЯЕТСЯ из БД ✅ (не затирается!)
---
3. Сохранение БЕЗ файлов (только answers):
SELECT * FROM ... WHERE ... = (
'{
"session_id": "sess_xxx",
"wizard_answers": {"q1": "answer1"},
"documents_meta": []
}'::jsonb,
'uuid-here'::text
);
Результат:
- status_code = 'draft' (т.к. docs_exist != true)
- wizard_plan сохраняется из БД ✅
============================================================================
ОЖИДАЕМЫЙ РЕЗУЛЬТАТ:
============================================================================
{
"claim": {
"claim_id": "uuid",
"claim_id_str": "uuid",
"status_code": "draft" or "in_work",
"unified_id": "usr_xxx",
"contact_id": "12345",
"phone": "79262306381",
"session_token": "sess_xxx",
"payload": {
"claim_id": "uuid",
"problem_description": "...",
"answers": {...},
"documents_meta": [...],
"wizard_plan": {...}, // ← СОХРАНЯЕТСЯ из БД!
"answers_prefill": [...],
"coverage_report": {...},
"ai_agent1_facts": {...},
"ai_agent13_rag": "..."
}
},
"documents": [...]
}
============================================================================
TROUBLESHOOTING:
============================================================================
Проблема: wizard_plan всё равно NULL после загрузки файлов
Причина: В n8n workflow form_get не передаётся claim_id в payload
Решение: Убедиться, что в set_token1 извлекается claim_id из webhook:
"claim_id": "={{ $('Webhook').item.json.body.claim_id }}"
Проблема: Затираются ai_agent1_facts после Step 3
Причина: Не включены в payload при отправке
Решение: SQL сохраняет их из БД автоматически ✅
*/