- Исправлена потеря документов при обновлении черновика (SQL объединяет вместо перезаписи) - Исправлено определение типа документа (приоритет field_label над field_name) - Исправлены дубликаты в documents_meta и documents_uploaded - Добавлена передача group_index с фронтенда для правильного field_name - Исправлены все документы в таблице clpr_claim_documents с правильными field_name - Обновлены SQL запросы: claimsave и claimsave_final для нового флоу - Добавлена поддержка multi-file upload для одного документа - Исправлены дубликаты в списке загруженных документов на фронтенде Файлы: - SQL: SQL_CLAIMSAVE_FIXED_NEW_FLOW.sql, SQL_CLAIMSAVE_FINAL_FIXED_NEW_FLOW_WITH_UPLOADED.sql - n8n: N8N_CODE_PROCESS_UPLOADED_FILES_FIXED.js (поддержка group_index) - Backend: documents.py (передача group_index в n8n) - Frontend: StepWizardPlan.tsx (передача group_index, исправление дубликатов) - Скрипты: fix_claim_documents_field_names.py, fix_documents_meta_duplicates.py Результат: документы больше не теряются, имеют правильные типы и field_name
363 lines
14 KiB
SQL
363 lines
14 KiB
SQL
-- ============================================================================
|
||
-- Исправленный SQL для сохранения claim (claimsave) - ПОДДЕРЖКА НОВОГО ФЛОУ
|
||
-- ============================================================================
|
||
-- Проблема: SQL не сохранял documents_required и перезаписывал status_code на 'draft'
|
||
-- Решение: Сохраняем documents_required и не перезаписываем новые статусы
|
||
-- ============================================================================
|
||
|
||
WITH partial AS (
|
||
SELECT
|
||
$1::jsonb AS p,
|
||
$2::text AS claim_id_str
|
||
),
|
||
|
||
existing_claim AS (
|
||
SELECT
|
||
id,
|
||
payload,
|
||
status_code,
|
||
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
|
||
),
|
||
|
||
-- Парсим documents_required (или берём из БД)
|
||
documents_required_parsed AS (
|
||
SELECT
|
||
CASE
|
||
WHEN partial.p->'documents_required' IS NOT NULL
|
||
AND jsonb_typeof(partial.p->'documents_required') = 'array'
|
||
THEN partial.p->'documents_required'
|
||
WHEN partial.p->'edit_fields_parsed'->'documents_required' IS NOT NULL
|
||
AND jsonb_typeof(partial.p->'edit_fields_parsed'->'documents_required') = 'array'
|
||
THEN partial.p->'edit_fields_parsed'->'documents_required'
|
||
WHEN EXISTS (SELECT 1 FROM existing_claim WHERE payload->'documents_required' IS NOT NULL)
|
||
THEN (SELECT payload->'documents_required' FROM existing_claim)
|
||
ELSE '[]'::jsonb
|
||
END AS documents_required
|
||
FROM partial
|
||
),
|
||
|
||
-- Парсим documents_uploaded (или берём из БД)
|
||
documents_uploaded_parsed AS (
|
||
SELECT
|
||
CASE
|
||
WHEN partial.p->'documents_uploaded' IS NOT NULL
|
||
AND jsonb_typeof(partial.p->'documents_uploaded') = 'array'
|
||
THEN partial.p->'documents_uploaded'
|
||
WHEN EXISTS (SELECT 1 FROM existing_claim WHERE payload->'documents_uploaded' IS NOT NULL)
|
||
THEN (SELECT payload->'documents_uploaded' FROM existing_claim)
|
||
ELSE '[]'::jsonb
|
||
END AS documents_uploaded
|
||
FROM partial
|
||
),
|
||
|
||
-- Парсим documents_skipped (или берём из БД)
|
||
documents_skipped_parsed AS (
|
||
SELECT
|
||
CASE
|
||
WHEN partial.p->'documents_skipped' IS NOT NULL
|
||
AND jsonb_typeof(partial.p->'documents_skipped') = 'array'
|
||
THEN partial.p->'documents_skipped'
|
||
WHEN EXISTS (SELECT 1 FROM existing_claim WHERE payload->'documents_skipped' IS NOT NULL)
|
||
THEN (SELECT payload->'documents_skipped' FROM existing_claim)
|
||
ELSE '[]'::jsonb
|
||
END AS documents_skipped
|
||
FROM partial
|
||
),
|
||
|
||
-- Парсим current_doc_index (или берём из БД)
|
||
current_doc_index_parsed AS (
|
||
SELECT
|
||
CASE
|
||
WHEN partial.p->'current_doc_index' IS NOT NULL
|
||
THEN (partial.p->'current_doc_index')::int
|
||
WHEN EXISTS (SELECT 1 FROM existing_claim WHERE payload->'current_doc_index' IS NOT NULL)
|
||
THEN (SELECT (payload->'current_doc_index')::int FROM existing_claim)
|
||
ELSE 0
|
||
END AS current_doc_index
|
||
FROM partial
|
||
),
|
||
|
||
-- Парсим wizard_answers
|
||
wizard_answers_parsed AS (
|
||
SELECT
|
||
CASE
|
||
WHEN partial.p->'edit_fields_raw'->'body'->>'wizard_answers' IS NOT NULL
|
||
THEN (partial.p->'edit_fields_raw'->'body'->>'wizard_answers')::jsonb
|
||
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
|
||
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'
|
||
WHEN partial.p->'edit_fields_raw'->'body'->>'wizard_plan' IS NOT NULL
|
||
THEN (partial.p->'edit_fields_raw'->'body'->>'wizard_plan')::jsonb
|
||
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
|
||
),
|
||
|
||
-- Парсим 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
|
||
),
|
||
|
||
-- Определяем правильный статус
|
||
status_code_resolved AS (
|
||
SELECT
|
||
CASE
|
||
-- Если есть documents_required и документы загружаются - новый флоу
|
||
WHEN (SELECT jsonb_array_length(documents_required) FROM documents_required_parsed) > 0
|
||
THEN CASE
|
||
-- Все документы загружены или пропущены
|
||
WHEN (SELECT jsonb_array_length(documents_uploaded) FROM documents_uploaded_parsed) +
|
||
(SELECT jsonb_array_length(documents_skipped) FROM documents_skipped_parsed) >=
|
||
(SELECT jsonb_array_length(documents_required) FROM documents_required_parsed)
|
||
THEN 'draft_docs_complete'
|
||
-- Документы загружаются
|
||
WHEN (SELECT jsonb_array_length(documents_uploaded) FROM documents_uploaded_parsed) > 0
|
||
THEN 'draft_docs_progress'
|
||
-- Только описание
|
||
ELSE 'draft_new'
|
||
END
|
||
-- Старый флоу: проверяем wizard_answers
|
||
WHEN (SELECT answers->>'docs_exist' FROM wizard_answers_parsed) = 'true'
|
||
THEN 'in_work'
|
||
-- Сохраняем существующий статус, если он новый
|
||
WHEN EXISTS (SELECT 1 FROM existing_claim
|
||
WHERE status_code IN ('draft_new', 'draft_docs_progress', 'draft_docs_complete', 'draft_claim_ready'))
|
||
THEN (SELECT status_code FROM existing_claim)
|
||
-- По умолчанию
|
||
ELSE 'draft'
|
||
END AS status_code
|
||
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
|
||
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'),
|
||
(SELECT status_code FROM status_code_resolved),
|
||
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 с существующими (не перезаписываем!)
|
||
'documents_meta', COALESCE(
|
||
(SELECT p->'documents_meta' FROM partial WHERE partial.p->'documents_meta' IS NOT NULL),
|
||
'[]'::jsonb
|
||
) || COALESCE(
|
||
(SELECT payload->'documents_meta' FROM existing_claim),
|
||
'[]'::jsonb
|
||
),
|
||
-- ✅ НОВЫЙ ФЛОУ: Сохраняем documents_required и связанные поля
|
||
'documents_required', (SELECT documents_required FROM documents_required_parsed),
|
||
'documents_uploaded', (SELECT documents_uploaded FROM documents_uploaded_parsed),
|
||
'documents_skipped', (SELECT documents_skipped FROM documents_skipped_parsed),
|
||
'current_doc_index', (SELECT current_doc_index FROM current_doc_index_parsed),
|
||
'wizard_plan', (SELECT wizard_plan FROM wizard_plan_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 = CASE
|
||
WHEN clpr_claims.status_code IN ('draft_new', 'draft_docs_progress', 'draft_docs_complete', 'draft_claim_ready')
|
||
THEN clpr_claims.status_code -- Сохраняем существующий новый статус
|
||
ELSE EXCLUDED.status_code -- Используем новый статус
|
||
END,
|
||
-- ✅ Объединяем payload правильно: аккуратно объединяем критичные поля
|
||
payload = jsonb_set(
|
||
jsonb_set(
|
||
jsonb_set(
|
||
jsonb_set(
|
||
jsonb_set(
|
||
-- Сначала берём существующий payload и объединяем с новым (без критичных полей)
|
||
COALESCE(clpr_claims.payload, '{}'::jsonb) ||
|
||
(EXCLUDED.payload - 'documents_meta' - 'documents_required' - 'documents_uploaded' - 'documents_skipped' - 'current_doc_index'),
|
||
'{documents_meta}',
|
||
-- ✅ ОБЪЕДИНЯЕМ documents_meta (не перезаписываем!)
|
||
COALESCE(
|
||
EXCLUDED.payload->'documents_meta',
|
||
'[]'::jsonb
|
||
) || COALESCE(
|
||
clpr_claims.payload->'documents_meta',
|
||
'[]'::jsonb
|
||
),
|
||
true
|
||
),
|
||
'{documents_required}',
|
||
COALESCE(
|
||
EXCLUDED.payload->'documents_required',
|
||
clpr_claims.payload->'documents_required',
|
||
'[]'::jsonb
|
||
),
|
||
true
|
||
),
|
||
'{documents_uploaded}',
|
||
COALESCE(
|
||
EXCLUDED.payload->'documents_uploaded',
|
||
clpr_claims.payload->'documents_uploaded',
|
||
'[]'::jsonb
|
||
),
|
||
true
|
||
),
|
||
'{documents_skipped}',
|
||
COALESCE(
|
||
EXCLUDED.payload->'documents_skipped',
|
||
clpr_claims.payload->'documents_skipped',
|
||
'[]'::jsonb
|
||
),
|
||
true
|
||
),
|
||
'{current_doc_index}',
|
||
COALESCE(
|
||
EXCLUDED.payload->'current_doc_index',
|
||
clpr_claims.payload->'current_doc_index',
|
||
to_jsonb(0)
|
||
),
|
||
true
|
||
),
|
||
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;
|
||
|