Database changes: - Add unified_id, contact_id, phone columns to clpr_claims table - Create indexes for fast lookup by these fields - Migrate existing data from payload to new columns - SQL migration: docs/SQL_ALTER_CLPR_CLAIMS_ADD_FIELDS.sql SQL improvements: - Simplify claimsave query: remove complex claim_lookup logic - Use UPSERT (INSERT ON CONFLICT) with known claim_id - Always return claim (fix NULL issue) - Store unified_id, contact_id, phone directly in table columns - SQL: docs/SQL_CLAIMSAVE_UPSERT_SIMPLE.sql Workflow enhancements: - Add branch for form submissions WITHOUT files - Create 6 new nodes: extract, prepare, save, redis, respond - Separate flow for has_files=false in IF node - Instructions: docs/N8N_FORM_GET_NO_FILES_INSTRUCTIONS.md - Node config: docs/N8N_FORM_GET_NO_FILES_BRANCH.json Migration stats: - Total claims: 81 - With unified_id: 77 - Migrated from payload: 2 Next steps: 1. Add 6 nodes to n8n workflow form_get (ID: 8ZVMTsuH7Cmw7snw) 2. Connect TRUE branch of IF node to extract_webhook_data_no_files 3. Test form submission without files 4. Verify PostgreSQL save and Redis event
228 lines
6.1 KiB
SQL
228 lines
6.1 KiB
SQL
-- Упрощённый UPSERT для сохранения claim с известным claim_id
|
||
-- Используется в n8n workflow: form_get (нода claimsave)
|
||
-- Дата: 2025-11-21
|
||
-- Описание: Простой INSERT/UPDATE для claim, т.к. claim_id уже известен
|
||
|
||
-- Входные параметры:
|
||
-- $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
|
||
),
|
||
|
||
-- Парсим wizard_answers
|
||
wizard_answers_parsed AS (
|
||
SELECT
|
||
CASE
|
||
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->>'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'
|
||
ELSE NULL
|
||
END AS wizard_plan
|
||
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
|
||
partial.claim_id_str::uuid,
|
||
COALESCE(partial.p->>'session_id', 'sess-unknown'),
|
||
partial.p->>'unified_id',
|
||
partial.p->>'contact_id',
|
||
partial.p->>'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,
|
||
'answers', (SELECT answers FROM wizard_answers_parsed),
|
||
'documents_meta', COALESCE(partial.p->'documents_meta', '[]'::jsonb),
|
||
'wizard_plan', (SELECT wizard_plan FROM wizard_plan_parsed)
|
||
),
|
||
COALESCE(
|
||
(SELECT created_at FROM clpr_claims WHERE id = partial.claim_id_str::uuid),
|
||
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 = (
|
||
-- Сохраняем старые поля, которых нет в новом payload
|
||
clpr_claims.payload
|
||
- 'answers'
|
||
- 'documents_meta'
|
||
- 'wizard_plan'
|
||
- 'claim_id'
|
||
) || 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;
|
||
|
||
/*
|
||
ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:
|
||
|
||
1. Вызов с wizard_answers и wizard_plan:
|
||
|
||
SELECT * FROM ... WHERE ... = (
|
||
'{
|
||
"session_id": "sess_xxx",
|
||
"unified_id": "usr_xxx",
|
||
"contact_id": "12345",
|
||
"phone": "79262306381",
|
||
"wizard_answers": "{\\"q1\\": \\"answer1\\"}",
|
||
"wizard_plan": "{\\"questions\\": [...]}",
|
||
"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-here'::text
|
||
);
|
||
|
||
2. Вызов БЕЗ файлов (только answers):
|
||
|
||
SELECT * FROM ... WHERE ... = (
|
||
'{
|
||
"session_id": "sess_xxx",
|
||
"unified_id": "usr_xxx",
|
||
"contact_id": "12345",
|
||
"phone": "79262306381",
|
||
"wizard_answers": "{\\"q1\\": \\"answer1\\"}",
|
||
"wizard_plan": null,
|
||
"documents_meta": []
|
||
}'::jsonb,
|
||
'uuid-here'::text
|
||
);
|
||
|
||
РЕЗУЛЬТАТ:
|
||
{
|
||
"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": {...}
|
||
},
|
||
"documents": [
|
||
{
|
||
"id": "uuid",
|
||
"field_name": "uploads[0][0]",
|
||
"file_id": "clientright/0/file.pdf",
|
||
"file_name": "file.pdf",
|
||
"original_file_name": "original.pdf"
|
||
}
|
||
]
|
||
}
|
||
*/
|
||
|