fix: Исправление загрузки документов и SQL запросов

- Исправлена потеря документов при обновлении черновика (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
This commit is contained in:
AI Assistant
2025-11-26 19:54:51 +03:00
parent 1d6c9d1f52
commit 02689e65db
42 changed files with 8314 additions and 232 deletions

View File

@@ -0,0 +1,345 @@
-- ============================================================================
-- SQL запрос для n8n: Сохранение черновика (НОВЫЙ ФЛОУ с документами)
-- ============================================================================
-- Назначение: Сохранить черновик сразу после анализа описания проблемы
-- AI Agent возвращает facts + docs (список документов)
--
-- Вход от AI Agent:
-- output: { facts_short, facts_full, problem, recommendation, docs: [...] }
-- propertyName: { session_id, phone, unified_id, contact_id, ФИО и т.д. }
--
-- Параметры:
-- $1 = payload_json (jsonb) - полный payload с output и propertyName
-- $2 = session_token (text) - сессия пользователя (из propertyName.session_id)
-- $3 = unified_id (text) - unified_id пользователя
-- $4 = problem_description (text) - исходное описание проблемы от пользователя
--
-- Возвращает:
-- claim - объект с claim_id, session_token, status_code, documents_required
-- ============================================================================
WITH input_data AS (
SELECT
$1::jsonb AS payload,
$2::text AS session_token_str,
NULLIF($3::text, '') AS unified_id_str,
NULLIF($4::text, '') AS problem_desc
),
-- Извлекаем данные из payload
parsed_data AS (
SELECT
input_data.*,
input_data.payload->'output' AS ai_output,
input_data.payload->'propertyName' AS user_data,
input_data.payload->'output'->'docs' AS documents_required
FROM input_data
),
-- Проверяем существующий черновик по session_token
existing_claim AS (
SELECT id, payload
FROM clpr_claims
WHERE session_token = (SELECT session_token_str FROM input_data)
LIMIT 1
),
-- Генерируем или используем существующий UUID
claim_id_resolved AS (
SELECT
COALESCE(
(SELECT id FROM existing_claim),
gen_random_uuid()
) AS claim_uuid
),
-- INSERT или UPDATE черновика
upserted_claim AS (
INSERT INTO clpr_claims (
id,
session_token,
unified_id,
channel,
type_code,
status_code,
payload,
created_at,
updated_at,
expires_at
)
SELECT
claim_id_resolved.claim_uuid,
parsed_data.session_token_str,
COALESCE(parsed_data.unified_id_str, parsed_data.user_data->>'unified_id'),
'web_form',
'consumer',
'draft_new', -- ✅ Новый статус: только описание + документы
jsonb_build_object(
'claim_id', claim_id_resolved.claim_uuid::text,
'problem_description', COALESCE(parsed_data.problem_desc, parsed_data.user_data->>'problem_description'),
-- AI анализ
'ai_analysis', jsonb_build_object(
'facts_short', parsed_data.ai_output->>'facts_short',
'facts_full', parsed_data.ai_output->>'facts_full',
'problem', parsed_data.ai_output->>'problem',
'recommendation', parsed_data.ai_output->>'recommendation'
),
-- ✅ Список необходимых документов (новое!)
'documents_required', COALESCE(parsed_data.documents_required, '[]'::jsonb),
'documents_uploaded', '[]'::jsonb,
'documents_skipped', '[]'::jsonb,
'current_doc_index', 0,
-- Данные пользователя
'phone', COALESCE(parsed_data.user_data->>'phone', ''),
'email', COALESCE(parsed_data.user_data->>'email', ''),
'contact_id', parsed_data.user_data->>'contact_id',
-- ФИО и паспортные данные (для заявления)
'applicant', jsonb_build_object(
'lastname', parsed_data.user_data->>'lastname',
'firstname', parsed_data.user_data->>'firstname',
'middle_name', parsed_data.user_data->>'middle_name',
'birthday', parsed_data.user_data->>'birthday',
'birthplace', parsed_data.user_data->>'birthplace',
'inn', parsed_data.user_data->>'inn',
'address', parsed_data.user_data->>'mailingstreet',
'zip', parsed_data.user_data->>'mailingzip'
),
-- Telegram ID если есть
'tg_id', parsed_data.user_data->>'tg_id',
-- Флаги готовности
'wizard_ready', false,
'claim_ready', false
),
now(),
now(),
now() + interval '14 days'
FROM parsed_data, claim_id_resolved
ON CONFLICT (id) DO UPDATE SET
unified_id = COALESCE(EXCLUDED.unified_id, clpr_claims.unified_id),
status_code = 'draft_new',
payload = clpr_claims.payload || EXCLUDED.payload,
updated_at = now(),
expires_at = now() + interval '14 days'
RETURNING id, session_token, status_code, payload
)
-- Возвращаем результат для n8n
SELECT
jsonb_build_object(
'claim_id', upserted_claim.id::text,
'session_token', upserted_claim.session_token,
'status_code', upserted_claim.status_code,
'documents_required', upserted_claim.payload->'documents_required',
'documents_count', jsonb_array_length(COALESCE(upserted_claim.payload->'documents_required', '[]'::jsonb))
) AS claim
FROM upserted_claim;
-- ============================================================================
-- Пример вызова в n8n (PostgreSQL Node):
-- ============================================================================
--
-- Параметры:
-- $1 = {{ JSON.stringify($json) }} -- Весь payload от AI Agent
-- $2 = {{ $json.propertyName.session_id }} -- session_token
-- $3 = {{ $json.propertyName.unified_id }} -- unified_id
-- $4 = {{ $node["Redis Trigger"].json.description }} -- Исходное описание проблемы
--
-- После выполнения SQL, в Code Node пушим в Redis:
--
-- const result = $input.first().json.claim;
--
-- return {
-- json: {
-- channel: `ocr_events:${result.session_token}`,
-- event: {
-- event_type: 'documents_list_ready',
-- claim_id: result.claim_id,
-- session_id: result.session_token,
-- documents_required: result.documents_required,
-- documents_count: result.documents_count,
-- timestamp: new Date().toISOString()
-- }
-- }
-- };
-- ============================================================================
-- SQL запрос для n8n: Сохранение черновика (НОВЫЙ ФЛОУ с документами)
-- ============================================================================
-- Назначение: Сохранить черновик сразу после анализа описания проблемы
-- AI Agent возвращает facts + docs (список документов)
--
-- Вход от AI Agent:
-- output: { facts_short, facts_full, problem, recommendation, docs: [...] }
-- propertyName: { session_id, phone, unified_id, contact_id, ФИО и т.д. }
--
-- Параметры:
-- $1 = payload_json (jsonb) - полный payload с output и propertyName
-- $2 = session_token (text) - сессия пользователя (из propertyName.session_id)
-- $3 = unified_id (text) - unified_id пользователя
-- $4 = problem_description (text) - исходное описание проблемы от пользователя
--
-- Возвращает:
-- claim - объект с claim_id, session_token, status_code, documents_required
-- ============================================================================
WITH input_data AS (
SELECT
$1::jsonb AS payload,
$2::text AS session_token_str,
NULLIF($3::text, '') AS unified_id_str,
NULLIF($4::text, '') AS problem_desc
),
-- Извлекаем данные из payload
parsed_data AS (
SELECT
input_data.*,
input_data.payload->'output' AS ai_output,
input_data.payload->'propertyName' AS user_data,
input_data.payload->'output'->'docs' AS documents_required
FROM input_data
),
-- Проверяем существующий черновик по session_token
existing_claim AS (
SELECT id, payload
FROM clpr_claims
WHERE session_token = (SELECT session_token_str FROM input_data)
LIMIT 1
),
-- Генерируем или используем существующий UUID
claim_id_resolved AS (
SELECT
COALESCE(
(SELECT id FROM existing_claim),
gen_random_uuid()
) AS claim_uuid
),
-- INSERT или UPDATE черновика
upserted_claim AS (
INSERT INTO clpr_claims (
id,
session_token,
unified_id,
channel,
type_code,
status_code,
payload,
created_at,
updated_at,
expires_at
)
SELECT
claim_id_resolved.claim_uuid,
parsed_data.session_token_str,
COALESCE(parsed_data.unified_id_str, parsed_data.user_data->>'unified_id'),
'web_form',
'consumer',
'draft_new', -- ✅ Новый статус: только описание + документы
jsonb_build_object(
'claim_id', claim_id_resolved.claim_uuid::text,
'problem_description', COALESCE(parsed_data.problem_desc, parsed_data.user_data->>'problem_description'),
-- AI анализ
'ai_analysis', jsonb_build_object(
'facts_short', parsed_data.ai_output->>'facts_short',
'facts_full', parsed_data.ai_output->>'facts_full',
'problem', parsed_data.ai_output->>'problem',
'recommendation', parsed_data.ai_output->>'recommendation'
),
-- ✅ Список необходимых документов (новое!)
'documents_required', COALESCE(parsed_data.documents_required, '[]'::jsonb),
'documents_uploaded', '[]'::jsonb,
'documents_skipped', '[]'::jsonb,
'current_doc_index', 0,
-- Данные пользователя
'phone', COALESCE(parsed_data.user_data->>'phone', ''),
'email', COALESCE(parsed_data.user_data->>'email', ''),
'contact_id', parsed_data.user_data->>'contact_id',
-- ФИО и паспортные данные (для заявления)
'applicant', jsonb_build_object(
'lastname', parsed_data.user_data->>'lastname',
'firstname', parsed_data.user_data->>'firstname',
'middle_name', parsed_data.user_data->>'middle_name',
'birthday', parsed_data.user_data->>'birthday',
'birthplace', parsed_data.user_data->>'birthplace',
'inn', parsed_data.user_data->>'inn',
'address', parsed_data.user_data->>'mailingstreet',
'zip', parsed_data.user_data->>'mailingzip'
),
-- Telegram ID если есть
'tg_id', parsed_data.user_data->>'tg_id',
-- Флаги готовности
'wizard_ready', false,
'claim_ready', false
),
now(),
now(),
now() + interval '14 days'
FROM parsed_data, claim_id_resolved
ON CONFLICT (id) DO UPDATE SET
unified_id = COALESCE(EXCLUDED.unified_id, clpr_claims.unified_id),
status_code = 'draft_new',
payload = clpr_claims.payload || EXCLUDED.payload,
updated_at = now(),
expires_at = now() + interval '14 days'
RETURNING id, session_token, status_code, payload
)
-- Возвращаем результат для n8n
SELECT
jsonb_build_object(
'claim_id', upserted_claim.id::text,
'session_token', upserted_claim.session_token,
'status_code', upserted_claim.status_code,
'documents_required', upserted_claim.payload->'documents_required',
'documents_count', jsonb_array_length(COALESCE(upserted_claim.payload->'documents_required', '[]'::jsonb))
) AS claim
FROM upserted_claim;
-- ============================================================================
-- Пример вызова в n8n (PostgreSQL Node):
-- ============================================================================
--
-- Параметры:
-- $1 = {{ JSON.stringify($json) }} -- Весь payload от AI Agent
-- $2 = {{ $json.propertyName.session_id }} -- session_token
-- $3 = {{ $json.propertyName.unified_id }} -- unified_id
-- $4 = {{ $node["Redis Trigger"].json.description }} -- Исходное описание проблемы
--
-- После выполнения SQL, в Code Node пушим в Redis:
--
-- const result = $input.first().json.claim;
--
-- return {
-- json: {
-- channel: `ocr_events:${result.session_token}`,
-- event: {
-- event_type: 'documents_list_ready',
-- claim_id: result.claim_id,
-- session_id: result.session_token,
-- documents_required: result.documents_required,
-- documents_count: result.documents_count,
-- timestamp: new Date().toISOString()
-- }
-- }
-- };
-- ============================================================================