Problem:
- After wizard form submission, need to wait for claim data from n8n
- Claim data comes via Redis channel claim:plan:{session_token}
- Need to display confirmation form with claim data
Solution:
1. Backend: Added SSE endpoint /api/v1/claim-plan/{session_token}
- Subscribes to Redis channel claim:plan:{session_token}
- Streams claim data from n8n to frontend
- Handles timeouts and errors gracefully
2. Frontend: Added subscription to claim:plan channel
- StepWizardPlan: After form submission, subscribes to SSE
- Waits for claim_plan_ready event
- Shows loading message while waiting
- On success: saves claimPlanData and shows confirmation step
3. New component: StepClaimConfirmation
- Displays claim confirmation form in iframe
- Receives claimPlanData from parent
- Generates HTML form (placeholder - should call n8n for real HTML)
- Handles confirmation/cancellation via postMessage
4. ClaimForm: Added conditional step for confirmation
- Shows StepClaimConfirmation when showClaimConfirmation=true
- Step appears after StepWizardPlan
- Only visible when claimPlanData is available
Flow:
1. User fills wizard form → submits
2. Form data sent to n8n via /api/v1/claims/wizard
3. Frontend subscribes to SSE /api/v1/claim-plan/{session_token}
4. n8n processes data → publishes to Redis claim:plan:{session_token}
5. Backend receives → streams to frontend via SSE
6. Frontend receives → shows StepClaimConfirmation
7. User confirms → proceeds to next step
Files:
- backend/app/api/events.py: Added stream_claim_plan endpoint
- frontend/src/components/form/StepWizardPlan.tsx: Added subscribeToClaimPlan
- frontend/src/components/form/StepClaimConfirmation.tsx: New component
- frontend/src/pages/ClaimForm.tsx: Added confirmation step to steps array
11 KiB
Инструкция: Добавление обработки формы БЕЗ файлов в workflow form_get
Дата: 2025-11-21
Workflow ID: 8ZVMTsuH7Cmw7snw
Workflow Name: form_get
🎯 Цель
Добавить ветку обработки для случая, когда форма отправляется без файлов (has_files === false).
📍 Где добавлять
В workflow form_get есть IF-нода "проверка наличия файлов" (ID: b7497f29-dab3-41cd-aaa3-a43ee83e607c):
- TRUE ветка (index 0) — файлов НЕТ → ПУСТАЯ ❌
- FALSE ветка (index 1) — файлы ЕСТЬ → существующий flow ✅
Задача: Добавить ноды в TRUE ветку.
📝 Пошаговая инструкция
Шаг 1: Открыть workflow
- Перейти в n8n: https://n8n.clientright.pro
- Открыть workflow "form_get"
- Найти ноду "проверка наличия файлов" (IF)
Шаг 2: Добавить ноду #1 - Extract Data
Название: extract_webhook_data_no_files
Тип: Edit Fields (Set)
Позиция: справа от IF-ноды, выше основного flow
Параметры:
| Field Name | Type | Value |
|---|---|---|
session_id |
String | ={{ $('Webhook').item.json.body.session_id }} |
claim_id |
String | ={{ $('Webhook').item.json.body.claim_id }} |
unified_id |
String | ={{ $('Webhook').item.json.body.unified_id }} |
contact_id |
String | ={{ $('Webhook').item.json.body.contact_id }} |
phone |
String | ={{ $('Webhook').item.json.body.phone }} |
wizard_plan |
Object | ={{ $('Webhook').item.json.body.wizard_plan }} |
wizard_answers |
Object | ={{ $('Webhook').item.json.body.wizard_answers }} |
type_code |
String | `={{ $('Webhook').item.json.body.type_code |
Подключение:
- Из ноды "проверка наличия файлов" → TRUE (верхний выход)
- К ноде "extract_webhook_data_no_files"
Шаг 3: Добавить ноду #2 - Prepare Payload
Название: prepare_payload_no_files
Тип: Edit Fields (Set)
Параметры:
| Field Name | Type | Value |
|---|---|---|
payload_partial_json |
Object | См. ниже ⬇️ |
claim_id |
String | ={{ $json.claim_id }} |
Значение для payload_partial_json:
={{ {
session_id: $json.session_id,
unified_id: $json.unified_id,
contact_id: $json.contact_id,
phone: $json.phone,
type_code: $json.type_code,
wizard_plan: $json.wizard_plan,
wizard_answers: $json.wizard_answers,
documents_meta: []
} }}
Подключение:
- Из ноды "extract_webhook_data_no_files"
- К ноде "prepare_payload_no_files"
Шаг 4: Добавить ноду #3 - Save to PostgreSQL
Название: save_claim_no_files
Тип: Postgres
Operation: Execute Query
Credentials: timeweb_bd (существующие)
Query:
WITH partial AS (
SELECT
$1::jsonb AS p,
$2::text AS claim_id_str
),
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_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
),
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'),
'draft',
jsonb_build_object(
'claim_id', partial.claim_id_str,
'answers', (SELECT answers FROM wizard_answers_parsed),
'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 = 'draft',
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
)
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;
Query Replacement: ={{ $json.payload_partial_json }}, {{ $json.claim_id }}
Подключение:
- Из ноды "prepare_payload_no_files"
- К ноде "save_claim_no_files"
Шаг 5: Добавить ноду #4 - Prepare Redis Event
Название: prepare_redis_event_no_files
Тип: Edit Fields (Set)
Параметры:
| Field Name | Type | Value |
|---|---|---|
redis_key |
String | =ocr_events:{{ $('extract_webhook_data_no_files').item.json.session_id }} |
redis_value |
Object | См. ниже ⬇️ |
Значение для redis_value:
={{ {
event_type: 'form_saved_no_files',
claim_id: $json.claim.claim_id,
status_code: $json.claim.status_code,
unified_id: $json.claim.unified_id,
contact_id: $json.claim.contact_id,
phone: $json.claim.phone,
session_token: $json.claim.session_token,
has_files: false,
timestamp: new Date().toISOString()
} }}
Подключение:
- Из ноды "save_claim_no_files"
- К ноде "prepare_redis_event_no_files"
Шаг 6: Добавить ноду #5 - Publish to Redis
Название: publish_to_redis_no_files
Тип: Redis
Operation: Publish
Credentials: Local Redis (существующие)
Параметры:
| Parameter | Value |
|---|---|
| Channel | ={{ $json.redis_key }} |
| Value | ={{ JSON.stringify($json.redis_value) }} |
Подключение:
- Из ноды "prepare_redis_event_no_files"
- К ноде "publish_to_redis_no_files"
Шаг 7: Добавить ноду #6 - Respond to Webhook
Название: respond_no_files
Тип: Respond to Webhook
Response Code: 200
Response Body:
={{ {
success: true,
claim_id: $('save_claim_no_files').item.json.claim.claim_id,
status_code: $('save_claim_no_files').item.json.claim.status_code,
has_files: false,
message: 'Заявка сохранена без файлов'
} }}
Подключение:
- Из ноды "publish_to_redis_no_files"
- К ноде "respond_no_files" (финальная нода)
🔄 Финальная структура workflow
Webhook → Code17 (парсинг файлов)
↓
IF "проверка наличия файлов"
│
├─ TRUE (файлов НЕТ) → [НОВАЯ ВЕТКА]
│ ↓
│ extract_webhook_data_no_files
│ ↓
│ prepare_payload_no_files
│ ↓
│ save_claim_no_files (PostgreSQL)
│ ↓
│ prepare_redis_event_no_files
│ ↓
│ publish_to_redis_no_files
│ ↓
│ respond_no_files
│
└─ FALSE (файлы ЕСТЬ) → существующий flow
↓
set_token1 → get_data1 → Upload → ...
✅ Проверка
После добавления нод:
- Сохранить workflow (Ctrl+S)
- Активировать workflow (если не активен)
- Протестировать:
- Отправить форму БЕЗ файлов
- Проверить, что заявка сохранилась в PostgreSQL
- Проверить событие в Redis:
redis-cli GET "ocr_events:sess-xxx" - Проверить ответ webhook:
success: true, has_files: false
📊 Ожидаемый результат
Вход (webhook body):
{
"session_id": "sess_xxx",
"claim_id": "uuid",
"unified_id": "usr_xxx",
"contact_id": "12345",
"phone": "79262306381",
"wizard_answers": {"q1": "answer1"},
"wizard_plan": null
}
Выход (claim в PostgreSQL):
{
"claim": {
"claim_id": "uuid",
"status_code": "draft",
"unified_id": "usr_xxx",
"contact_id": "12345",
"phone": "79262306381",
"session_token": "sess_xxx",
"payload": {
"claim_id": "uuid",
"answers": {"q1": "answer1"},
"documents_meta": [],
"wizard_plan": null
}
}
}
Redis событие:
{
"event_type": "form_saved_no_files",
"claim_id": "uuid",
"status_code": "draft",
"has_files": false,
"timestamp": "2025-11-21T15:00:00.000Z"
}
🛠️ Troubleshooting
Проблема 1: "session_token": "sess-unknown"
Причина: В payload не передан session_id
Решение: Проверить, что фронтенд отправляет session_id в body
Проблема 2: "contact_id": null
Причина: Поле не извлекается из webhook
Решение: Проверить путь в expression: $('Webhook').item.json.body.contact_id
Проблема 3: Ошибка PostgreSQL
Причина: Неправильный формат данных
Решение: Проверить логи n8n и формат payload_partial_json
Автор: AI Assistant
Дата: 2025-11-21
Статус: Готово к внедрению ✅