fix: Preserve wizard_plan and AI fields from DB when updating claim

Problem:
- When uploading files on Step 3, wizard_plan was reset to NULL
- wizard_plan is created on Step 2 (StepDescription) and saved by claimsave_primary
- form_get workflow on Step 3 doesn't receive wizard_plan again
- Old SQL was overwriting wizard_plan with NULL

Solution:
- Add 'existing_claim' CTE to read current payload from DB
- Modified all parsers to fallback to DB values if field not in incoming payload:
  * wizard_plan_parsed - preserves generated wizard plan
  * answers_prefill_parsed - preserves AI-generated prefill
  * coverage_report_parsed - preserves coverage analysis
  * ai_agent1_facts_parsed - preserves fact extraction
  * ai_agent13_rag_parsed - preserves RAG analysis
  * problem_description_parsed - preserves user description

Flow:
1. Step 2: User describes problem → claimsave_primary saves wizard_plan 
2. Step 3: User uploads files → form_get/claimsave preserves wizard_plan from DB 

File: docs/SQL_CLAIMSAVE_UPSERT_SIMPLE.sql
Next: Update n8n workflow 'form_get' node 'claimsave' with this SQL
This commit is contained in:
AI Assistant
2025-11-21 16:04:55 +03:00
parent d6b17baa7d
commit 60a67c7e37

View File

@@ -1,7 +1,8 @@
-- Упрощённый UPSERT для сохранения claim с известным claim_id
-- Используется в n8n workflow: form_get (нода claimsave)
-- Дата: 2025-11-21
-- Дата: 2025-11-21 (обновлено: сохранение существующих полей)
-- Описание: Простой INSERT/UPDATE для claim, т.к. claim_id уже известен
-- ВАЖНО: Сохраняет wizard_plan и другие поля из БД, если не пришли новые
-- Входные параметры:
-- $1: payload_partial_json (jsonb) - данные формы с wizard_answers, wizard_plan, documents_meta
@@ -13,6 +14,16 @@ WITH partial AS (
$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
@@ -27,20 +38,94 @@ wizard_answers_parsed AS (
FROM partial
),
-- Парсим wizard_plan
-- Парсим wizard_plan (или берём из существующей записи)
wizard_plan_parsed AS (
SELECT
CASE
-- Сначала пытаемся взять из входящих данных
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'
-- Если нет в 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 (
@@ -72,9 +157,16 @@ claim_upsert AS (
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)
'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),
@@ -89,14 +181,7 @@ claim_upsert AS (
contact_id = COALESCE(EXCLUDED.contact_id, clpr_claims.contact_id),
phone = COALESCE(EXCLUDED.phone, clpr_claims.phone),
status_code = EXCLUDED.status_code,
payload = (
-- Сохраняем старые поля, которых нет в новом payload
clpr_claims.payload
- 'answers'
- 'documents_meta'
- 'wizard_plan'
- 'claim_id'
) || EXCLUDED.payload,
payload = EXCLUDED.payload,
updated_at = now(),
expires_at = now() + interval '14 days'
RETURNING id, status_code, payload, unified_id, contact_id, phone, session_token
@@ -161,9 +246,21 @@ SELECT
)) FROM docs_upsert) AS documents;
/*
ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:
============================================================================
КЛЮЧЕВЫЕ ИЗМЕНЕНИЯ (2025-11-21):
============================================================================
1. Вызов с wizard_answers и wizard_plan:
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 ... = (
'{
@@ -171,8 +268,27 @@ SELECT * FROM ... WHERE ... = (
"unified_id": "usr_xxx",
"contact_id": "12345",
"phone": "79262306381",
"wizard_answers": "{\\"q1\\": \\"answer1\\"}",
"wizard_plan": "{\\"questions\\": [...]}",
"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]",
@@ -183,25 +299,35 @@ SELECT * FROM ... WHERE ... = (
}
]
}'::jsonb,
'uuid-here'::text
'СУЩЕСТВУЮЩИЙ-uuid'::text
);
2. Вызов БЕЗ файлов (только answers):
Результат:
- Обновляется answers ✅
- Добавляются documents_meta ✅
- wizard_plan СОХРАНЯЕТСЯ из БД ✅ (не затирается!)
---
3. Сохранение БЕЗ файлов (только answers):
SELECT * FROM ... WHERE ... = (
'{
"session_id": "sess_xxx",
"unified_id": "usr_xxx",
"contact_id": "12345",
"phone": "79262306381",
"wizard_answers": "{\\"q1\\": \\"answer1\\"}",
"wizard_plan": null,
"wizard_answers": {"q1": "answer1"},
"documents_meta": []
}'::jsonb,
'uuid-here'::text
);
РЕЗУЛЬТАТ:
Результат:
- status_code = 'draft' (т.к. docs_exist != true)
- wizard_plan сохраняется из БД ✅
============================================================================
ОЖИДАЕМЫЙ РЕЗУЛЬТАТ:
============================================================================
{
"claim": {
"claim_id": "uuid",
@@ -211,17 +337,32 @@ SELECT * FROM ... WHERE ... = (
"contact_id": "12345",
"phone": "79262306381",
"session_token": "sess_xxx",
"payload": {...}
},
"documents": [
{
"id": "uuid",
"field_name": "uploads[0][0]",
"file_id": "clientright/0/file.pdf",
"file_name": "file.pdf",
"original_file_name": "original.pdf"
"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 сохраняет их из БД автоматически ✅
*/