Files
aiform_prod/docs/CLAIMSAVE_FINAL_SQL.md
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

6.9 KiB
Raw Permalink Blame History

Исправленный SQL для ноды claimsave_final

Текущая проблема

Нода claimsave_final использует $2::uuid, но получает строку "CLM-2025-11-18-GEQ3KL", что вызывает ошибку.

Особенности claimsave_final

  1. Используется после конвертации файлов в PDF и загрузки в S3
  2. Работает с file_url (URL файла в S3)
  3. Обновляет только documents_meta в payload (не трогает answers)
  4. Использует динамический префикс таблицы (для разных схем)

Исправленный SQL запрос

-- $1 = payload_partial_json (jsonb)
-- $2 = claim_id (text, например "CLM-2025-11-18-GEQ3KL")

WITH partial AS (
  SELECT $1::jsonb AS p, $2::text AS claim_id_str
),

-- Находим UUID по строковому claim_id
claim_lookup AS (
  SELECT 
    COALESCE(
      (SELECT id FROM clpr_claims WHERE payload->>'claim_id' = partial.claim_id_str LIMIT 1),
      gen_random_uuid()
    ) AS claim_uuid
  FROM partial
),

-- Если записи нет, создаем её (на всякий случай)
claim_created AS (
  INSERT INTO clpr_claims (
    id,
    session_token,
    channel,
    type_code,
    status_code,
    payload,
    created_at,
    updated_at,
    expires_at
  )
  SELECT 
    claim_lookup.claim_uuid,
    COALESCE(partial.p->>'session_id', 'sess-' || gen_random_uuid()::text),
    'web_form',
    COALESCE(partial.p->>'type_code', 'consumer'),
    'draft',
    jsonb_build_object(
      'claim_id', partial.claim_id_str,
      'documents_meta', COALESCE(partial.p->'documents_meta', '[]'::jsonb)
    ),
    now(),
    now(),
    now() + interval '14 days'
  FROM partial, claim_lookup
  WHERE NOT EXISTS (
    SELECT 1 FROM clpr_claims WHERE id = claim_lookup.claim_uuid
  )
  ON CONFLICT (id) DO NOTHING
  RETURNING id
),

-- Получаем финальный UUID
claim_final AS (
  SELECT 
    CASE 
      WHEN EXISTS (SELECT 1 FROM claim_created) 
        THEN (SELECT id FROM claim_created LIMIT 1)
      ELSE claim_lookup.claim_uuid
    END AS claim_uuid
  FROM claim_lookup
),

-- Извлекаем документы из payload
docs AS (
  SELECT
    claim_final.claim_uuid::text AS claim_id,  -- преобразуем UUID в строку для clpr_claim_documents
    doc.field_name::text,
    doc.file_id::text,
    doc.file_name::text,
    doc.original_file_name::text,
    (doc.uploaded_at)::timestamptz AS uploaded_at,
    doc.file_url::text
  FROM partial, claim_final
  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,
    file_url text
  )
),

-- Сохраняем/обновляем документы
upsert_docs AS (
  INSERT INTO clpr_claim_documents
    (claim_id, field_name, file_id, uploaded_at, file_name, original_file_name)
  SELECT 
    claim_id,
    field_name,
    file_id,
    uploaded_at,
    file_name,
    original_file_name
  FROM docs
  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
),

-- Обновляем payload (только documents_meta, не трогаем answers)
upd_claim AS (
  UPDATE clpr_claims c
  SET 
    payload = jsonb_set(
      COALESCE(c.payload, '{}'::jsonb),
      '{documents_meta}',
      COALESCE((SELECT p->'documents_meta' FROM partial), '[]'::jsonb),
      true
    ),
    updated_at = now(),
    expires_at = now() + interval '14 days'
  FROM partial, claim_final
  WHERE c.id = claim_final.claim_uuid
  RETURNING c.id, c.payload
)

SELECT
  (SELECT jsonb_build_object(
    'claim_id', u.id::text,
    'claim_id_str', (u.payload->>'claim_id'),
    'payload', u.payload
  ) FROM upd_claim u LIMIT 1) AS claim,
  (
    SELECT jsonb_agg(
      jsonb_build_object(
        'id', u.id,
        'field_name', u.field_name,
        'file_id', u.file_id,
        'file_url', d.file_url,
        'file_name', d.file_name,
        'original_file_name', d.original_file_name,
        'uploaded_at', d.uploaded_at,
        -- имя, которое безопасно отдавать во внешний API
        'filename_for_upload',
          COALESCE(
            NULLIF(d.original_file_name, ''),
            NULLIF(d.file_name, ''),
            regexp_replace(d.file_id, '^.*/', '') -- хвост пути как запасной
          )
      )
    )
    FROM upsert_docs u
    JOIN docs d
      ON d.claim_id = u.claim_id
     AND d.field_name = u.field_name
    WHERE d.file_url IS NOT NULL AND d.file_url <> ''  -- не показываем без URL
  ) AS documents;

Изменения

  1. $2::text вместо $2::uuid: Принимает строковый claim_id
  2. claim_lookup CTE: Находит UUID по строковому claim_id из payload->>'claim_id'
  3. claim_created CTE: Создает запись, если её нет (на всякий случай)
  4. claim_final CTE: Получает финальный UUID (из созданной или существующей записи)
  5. docs CTE: Преобразует UUID в строку для clpr_claim_documents (т.к. там claim_id имеет тип character varying)
  6. Убраны динамические префиксы: Используется clpr_claims и clpr_claim_documents напрямую

Параметры запроса

В n8n PostgreSQL Node:

Parameters:
$1 = {{ $json.payload_partial_json }}  (JSONB)
$2 = {{ $json.claim_id }}              (TEXT, строка "CLM-2025-11-18-GEQ3KL")

Если нужен динамический префикс

Если всё-таки нужен динамический префикс таблицы (как в оригинале), можно использовать:

-- Вместо clpr_claims использовать:
{{ $('Edit Fields').item.json.propertyName.prefix }}claims

-- Вместо clpr_claim_documents использовать:
{{ $('Edit Fields').item.json.propertyName.prefix }}claim_documents

Но для ticket_form это не нужно, т.к. мы всегда работаем с clpr_* таблицами.

Отличия от claimsave

  1. claimsave: Сохраняет данные визарда (answers, wizard_plan, wizard_answers)
  2. claimsave_final: Обновляет только documents_meta после обработки файлов, добавляет file_url

Оба запроса теперь используют строковый claim_id и правильно находят UUID.