Files
aiform_prod/docs/N8N_FORM_GET_NO_FILES_BRANCH.json
AI Assistant 0978e485dc feat: Add claim plan confirmation flow via Redis SSE
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
2025-11-24 13:36:14 +03:00

263 lines
11 KiB
JSON

{
"meta": {
"description": "Ноды для обработки формы БЕЗ файлов в workflow form_get",
"date": "2025-11-21",
"action": "Добавить в TRUE ветку IF-ноды 'проверка наличия файлов'"
},
"nodes": [
{
"name": "extract_webhook_data_no_files",
"description": "Извлекаем данные из webhook для случая без файлов",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [-320, 400],
"parameters": {
"assignments": {
"assignments": [
{
"id": "session_id",
"name": "session_id",
"value": "={{ $('Webhook').item.json.body.session_id }}",
"type": "string"
},
{
"id": "claim_id",
"name": "claim_id",
"value": "={{ $('Webhook').item.json.body.claim_id }}",
"type": "string"
},
{
"id": "unified_id",
"name": "unified_id",
"value": "={{ $('Webhook').item.json.body.unified_id }}",
"type": "string"
},
{
"id": "contact_id",
"name": "contact_id",
"value": "={{ $('Webhook').item.json.body.contact_id }}",
"type": "string"
},
{
"id": "phone",
"name": "phone",
"value": "={{ $('Webhook').item.json.body.phone }}",
"type": "string"
},
{
"id": "wizard_plan",
"name": "wizard_plan",
"value": "={{ $('Webhook').item.json.body.wizard_plan }}",
"type": "object"
},
{
"id": "wizard_answers",
"name": "wizard_answers",
"value": "={{ $('Webhook').item.json.body.wizard_answers }}",
"type": "object"
},
{
"id": "type_code",
"name": "type_code",
"value": "={{ $('Webhook').item.json.body.type_code || 'consumer' }}",
"type": "string"
}
]
},
"options": {}
}
},
{
"name": "prepare_payload_no_files",
"description": "Формируем payload для PostgreSQL",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [-80, 400],
"parameters": {
"assignments": {
"assignments": [
{
"id": "payload_partial_json",
"name": "payload_partial_json",
"value": "={{ {\n session_id: $json.session_id,\n unified_id: $json.unified_id,\n contact_id: $json.contact_id,\n phone: $json.phone,\n type_code: $json.type_code,\n wizard_plan: $json.wizard_plan,\n wizard_answers: $json.wizard_answers,\n documents_meta: []\n} }}",
"type": "object"
},
{
"id": "claim_id",
"name": "claim_id",
"value": "={{ $json.claim_id }}",
"type": "string"
}
]
},
"options": {}
}
},
{
"name": "save_claim_no_files",
"description": "Сохраняем claim без файлов в PostgreSQL",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [180, 400],
"parameters": {
"operation": "executeQuery",
"query": "WITH partial AS (\n SELECT \n $1::jsonb AS p, \n $2::text AS claim_id_str\n),\n\n-- Парсим wizard_answers\nwizard_answers_parsed AS (\n SELECT \n CASE \n WHEN partial.p->>'wizard_answers' IS NOT NULL \n THEN (partial.p->>'wizard_answers')::jsonb\n WHEN partial.p->'wizard_answers' IS NOT NULL \n AND jsonb_typeof(partial.p->'wizard_answers') = 'object'\n THEN partial.p->'wizard_answers'\n ELSE '{}'::jsonb\n END AS answers\n FROM partial\n),\n\n-- Парсим wizard_plan\nwizard_plan_parsed AS (\n SELECT \n CASE \n WHEN partial.p->>'wizard_plan' IS NOT NULL \n THEN (partial.p->>'wizard_plan')::jsonb\n WHEN partial.p->'wizard_plan' IS NOT NULL \n AND jsonb_typeof(partial.p->'wizard_plan') = 'object'\n THEN partial.p->'wizard_plan'\n ELSE NULL\n END AS wizard_plan\n FROM partial\n),\n\n-- UPSERT claim\nclaim_upsert AS (\n INSERT INTO clpr_claims (\n id,\n session_token,\n unified_id,\n contact_id,\n phone,\n channel,\n type_code,\n status_code,\n payload,\n created_at,\n updated_at,\n expires_at\n )\n SELECT \n partial.claim_id_str::uuid,\n COALESCE(partial.p->>'session_id', 'sess-unknown'),\n partial.p->>'unified_id',\n partial.p->>'contact_id',\n partial.p->>'phone',\n 'web_form',\n COALESCE(partial.p->>'type_code', 'consumer'),\n 'draft',\n jsonb_build_object(\n 'claim_id', partial.claim_id_str,\n 'answers', (SELECT answers FROM wizard_answers_parsed),\n 'documents_meta', '[]'::jsonb,\n 'wizard_plan', (SELECT wizard_plan FROM wizard_plan_parsed)\n ),\n COALESCE(\n (SELECT created_at FROM clpr_claims WHERE id = partial.claim_id_str::uuid),\n now()\n ),\n now(),\n now() + interval '14 days'\n FROM partial\n ON CONFLICT (id) DO UPDATE SET\n session_token = EXCLUDED.session_token,\n unified_id = COALESCE(EXCLUDED.unified_id, clpr_claims.unified_id),\n contact_id = COALESCE(EXCLUDED.contact_id, clpr_claims.contact_id),\n phone = COALESCE(EXCLUDED.phone, clpr_claims.phone),\n status_code = 'draft',\n payload = (\n clpr_claims.payload \n - 'answers' \n - 'documents_meta' \n - 'wizard_plan' \n - 'claim_id'\n ) || EXCLUDED.payload,\n updated_at = now(),\n expires_at = now() + interval '14 days'\n RETURNING id, status_code, payload, unified_id, contact_id, phone, session_token\n)\n\nSELECT\n (SELECT jsonb_build_object(\n 'claim_id', cu.id::text,\n 'claim_id_str', (cu.payload->>'claim_id'),\n 'status_code', cu.status_code,\n 'unified_id', cu.unified_id,\n 'contact_id', cu.contact_id,\n 'phone', cu.phone,\n 'session_token', cu.session_token,\n 'payload', cu.payload\n ) FROM claim_upsert cu) AS claim;",
"options": {
"queryReplacement": "={{ $json.payload_partial_json }}, {{ $json.claim_id }}"
}
},
"credentials": {
"postgres": {
"id": "sGJ0fJhU8rz88w3k",
"name": "timeweb_bd"
}
}
},
{
"name": "prepare_redis_event_no_files",
"description": "Готовим событие для публикации в Redis",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [440, 400],
"parameters": {
"assignments": {
"assignments": [
{
"id": "redis_key",
"name": "redis_key",
"value": "=ocr_events:{{ $('extract_webhook_data_no_files').item.json.session_id }}",
"type": "string"
},
{
"id": "redis_value",
"name": "redis_value",
"value": "={{ {\n event_type: 'form_saved',\n claim_id: $json.claim.claim_id,\n status_code: $json.claim.status_code,\n unified_id: $json.claim.unified_id,\n contact_id: $json.claim.contact_id,\n phone: $json.claim.phone,\n session_token: $json.claim.session_token,\n has_files: false,\n timestamp: new Date().toISOString()\n} }}",
"type": "object"
}
]
},
"options": {}
}
},
{
"name": "publish_to_redis_no_files",
"description": "Публикуем событие в Redis",
"type": "n8n-nodes-base.redis",
"typeVersion": 1,
"position": [700, 400],
"parameters": {
"operation": "publish",
"channel": "={{ $json.redis_key }}",
"value": "={{ JSON.stringify($json.redis_value) }}",
"options": {}
},
"credentials": {
"redis": {
"id": "RKICQB2ZaisVK4WS",
"name": "Local Redis"
}
}
},
{
"name": "respond_no_files",
"description": "Возвращаем ответ клиенту",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [960, 400],
"parameters": {
"options": {
"responseCode": 200
},
"respondWith": "json",
"responseBody": "={{ {\n success: true,\n claim_id: $('save_claim_no_files').item.json.claim.claim_id,\n status_code: $('save_claim_no_files').item.json.claim.status_code,\n has_files: false,\n message: 'Заявка сохранена без файлов'\n} }}"
}
}
],
"connections": {
"проверка наличия файлов": {
"main": [
[
{
"node": "extract_webhook_data_no_files",
"type": "main",
"index": 0
}
],
[
{
"node": "set_token1",
"type": "main",
"index": 0
}
]
]
},
"extract_webhook_data_no_files": {
"main": [
[
{
"node": "prepare_payload_no_files",
"type": "main",
"index": 0
}
]
]
},
"prepare_payload_no_files": {
"main": [
[
{
"node": "save_claim_no_files",
"type": "main",
"index": 0
}
]
]
},
"save_claim_no_files": {
"main": [
[
{
"node": "prepare_redis_event_no_files",
"type": "main",
"index": 0
}
]
]
},
"prepare_redis_event_no_files": {
"main": [
[
{
"node": "publish_to_redis_no_files",
"type": "main",
"index": 0
}
]
]
},
"publish_to_redis_no_files": {
"main": [
[
{
"node": "respond_no_files",
"type": "main",
"index": 0
}
]
]
}
},
"instructions": {
"step1": "Открыть workflow 'form_get' (ID: 8ZVMTsuH7Cmw7snw) в n8n",
"step2": "Найти IF-ноду 'проверка наличия файлов' (ID: b7497f29-dab3-41cd-aaa3-a43ee83e607c)",
"step3": "TRUE ветка (index 0) сейчас пустая - туда нужно добавить новые ноды",
"step4": "Импортировать ноды из этого JSON или создать вручную по схеме",
"step5": "Подключить TRUE ветку IF к первой ноде: extract_webhook_data_no_files",
"step6": "Сохранить и активировать workflow",
"step7": "Протестировать отправку формы БЕЗ файлов"
}
}