Files
aiform_dev/docs/SQL_CLAIMSAVE_UPSERT_SIMPLE.sql
AI Assistant 33de3955ac fix: Add created_at to existing_claim CTE to fix PostgreSQL error
Error:
- column "created_at" does not exist
- There is a column named "created_at" in table "clpr_claims", but it cannot be referenced from this part of the query

Root Cause:
- existing_claim CTE only selected id and payload
- But INSERT query tried to use: (SELECT created_at FROM existing_claim)
- PostgreSQL couldn't find created_at in existing_claim CTE

Solution:
- Added created_at to existing_claim CTE SELECT clause
- Now created_at is available for use in INSERT query

Files:
- docs/SQL_CLAIMSAVE_UPSERT_SIMPLE.sql: Added created_at to existing_claim CTE
2025-11-24 16:59:33 +03:00

428 lines
16 KiB
SQL
Raw Permalink 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 из БД (если запись есть)
-- ✅ ИСПРАВЛЕНО: Ищем и по ID, и по payload->>'claim_id', чтобы избежать дубликатов
existing_claim AS (
SELECT
id,
payload,
created_at
FROM clpr_claims
WHERE id = (SELECT claim_id_str::uuid FROM partial)
OR payload->>'claim_id' = (SELECT claim_id_str FROM partial)
ORDER BY
CASE WHEN id = (SELECT claim_id_str::uuid FROM partial) THEN 1 ELSE 2 END,
updated_at DESC
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
-- ✅ ИСПРАВЛЕНО: Используем ID из existing_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
COALESCE(
(SELECT id FROM existing_claim),
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 existing_claim),
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)
============================================================================
ИСПРАВЛЕНИЕ ДУБЛИКАТОВ (2025-11-24):
============================================================================
ПРОБЛЕМА: Создавались дубликаты записей с одинаковым claim_id, но разными ID
Причина: Разные SQL запросы использовали разные подходы:
- Одни использовали claim_id как UUID для ID (partial.claim_id_str::uuid)
- Другие искали по payload->>'claim_id' и создавали новый UUID, если не находили
РЕШЕНИЕ:
1. existing_claim теперь ищет и по ID, и по payload->>'claim_id':
WHERE id = claim_id_str::uuid OR payload->>'claim_id' = claim_id_str
2. INSERT использует ID из existing_claim, если он найден:
COALESCE((SELECT id FROM existing_claim), partial.claim_id_str::uuid)
3. Это гарантирует, что всегда используется одна и та же запись
============================================================================
ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:
============================================================================
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 сохраняет их из БД автоматически ✅
*/