feat: Add PostgreSQL fields and workflow for form without files

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
This commit is contained in:
AI Assistant
2025-11-21 15:57:18 +03:00
parent 3621ae6021
commit d6b17baa7d
23 changed files with 963 additions and 0 deletions

View File

@@ -0,0 +1,227 @@
-- Упрощённый 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"
}
]
}
*/