feat: Получение cf_2624 из MySQL и блокировка полей при подтверждении данных

- Добавлен сервис CrmMySQLService для прямого подключения к MySQL CRM
- Обновлён метод get_draft() для получения cf_2624 напрямую из БД
- Реализована блокировка полей (readonly) при contact_data_confirmed = true
- Добавлен выбор банка для СБП выплат с динамической загрузкой из API
- Обновлена документация по работе с cf_2624 и MySQL
- Добавлен network_mode: host в docker-compose для доступа к MySQL
- Обновлены компоненты формы для поддержки блокировки полей
This commit is contained in:
AI Assistant
2025-12-04 12:22:23 +03:00
parent 64385c430d
commit 080e7ec105
69 changed files with 17034 additions and 1439 deletions

View File

@@ -0,0 +1,391 @@
-- ============================================================================
-- Исправленный SQL для сохранения claim (claimsave) - С ДЕДУПЛИКАЦИЕЙ
-- ============================================================================
-- Проблема: documents_meta накапливает дубликаты при каждом сохранении
-- Решение: При добавлении новых записей удаляем старые с тем же field_name
-- ============================================================================
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_meta
-- Приоритет: новые записи перезаписывают старые с тем же field_name
documents_meta_dedup AS (
SELECT COALESCE(
(
SELECT jsonb_agg(doc ORDER BY (doc->>'uploaded_at') DESC NULLS LAST)
FROM (
-- Уникальные записи: приоритет новым по field_name
SELECT DISTINCT ON (doc->>'field_name') doc
FROM (
-- 1. Сначала новые записи (приоритет)
SELECT jsonb_array_elements(
COALESCE((SELECT p->'documents_meta' FROM partial WHERE p->'documents_meta' IS NOT NULL), '[]'::jsonb)
) AS doc, 1 AS priority
UNION ALL
-- 2. Потом существующие записи
SELECT jsonb_array_elements(
COALESCE((SELECT payload->'documents_meta' FROM existing_claim), '[]'::jsonb)
) AS doc, 2 AS priority
) all_docs
-- Сортируем: сначала новые (priority=1), потом по дате
ORDER BY doc->>'field_name', priority, (doc->>'uploaded_at') DESC NULLS LAST
) unique_docs
),
'[]'::jsonb
) AS documents_meta
),
-- Парсим 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', (SELECT documents_meta FROM documents_meta_dedup),
-- ✅ НОВЫЙ ФЛОУ: Сохраняем 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,
-- ✅ ИСПРАВЛЕНО: Дедуплицированное объединение documents_meta
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}',
-- ✅ ДЕДУПЛИКАЦИЯ: новые записи перезаписывают старые с тем же field_name
(
SELECT COALESCE(jsonb_agg(doc), '[]'::jsonb)
FROM (
SELECT DISTINCT ON (doc->>'field_name') doc
FROM (
-- Новые записи (приоритет)
SELECT jsonb_array_elements(COALESCE(EXCLUDED.payload->'documents_meta', '[]'::jsonb)) AS doc, 1 AS priority
UNION ALL
-- Существующие записи
SELECT jsonb_array_elements(COALESCE(clpr_claims.payload->'documents_meta', '[]'::jsonb)) AS doc, 2 AS priority
) all_docs
ORDER BY doc->>'field_name', priority, (doc->>'uploaded_at') DESC NULLS LAST
) unique_docs
),
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;