diff --git a/backend/app/api/claims.py b/backend/app/api/claims.py index 701e128..f0d07fd 100644 --- a/backend/app/api/claims.py +++ b/backend/app/api/claims.py @@ -542,32 +542,40 @@ async def load_wizard_data(claim_id: str): @router.post("/approve") -async def approve_claim_form(request: Request): +async def publish_form_approval(request: Request): """ - Сохранение данных подтвержденной формы после SMS-апрува + Публикация данных подтвержденной формы в Redis канал - Принимает отредактированные данные формы подтверждения и отправляет их в n8n webhook. + После SMS-апрува отправляет данные формы в Redis канал form_approve:{claim_id} + для обработки в n8n workflow. + + В будущем можно подключить RabbitMQ для очереди и защиты от дублей. """ try: body = await request.json() - - logger.info( - "📨 TicketForm approval received", - extra={ - "claim_id": body.get("claim_id"), - "session_id": body.get("session_id"), - }, - ) - - # Формируем payload для n8n - n8n_payload = { - "stage": "form_approve", - "form_id": "ticket_form", - "session_id": body.get("session_id"), - "claim_id": body.get("claim_id"), + + claim_id = body.get("claim_id") + session_token = body.get("session_token") or body.get("session_id") + + if not claim_id: + raise HTTPException(status_code=400, detail="claim_id обязателен") + + # Генерируем idempotency key для защиты от дублей (для будущей интеграции с RabbitMQ) + import time + idempotency_key = f"{claim_id}_{int(time.time() * 1000)}_{body.get('user_id', 'unknown')}" + + # Формируем событие для Redis + event_data = { + "event_type": "form_approve", + "status": "approved", + "message": "Форма подтверждена после SMS-верификации", + "claim_id": claim_id, + "session_token": session_token, "unified_id": body.get("unified_id"), "phone": body.get("phone"), - "sms_verified": True, # Флаг что SMS код подтвержден + "sms_verified": True, + "idempotency_key": idempotency_key, # Для защиты от дублей в RabbitMQ + "timestamp": datetime.utcnow().isoformat(), # Данные формы подтверждения "form_data": body.get("form_data", {}), @@ -579,51 +587,34 @@ async def approve_claim_form(request: Request): # Оригинальные данные для сравнения "original_data": body.get("original_data", {}), } - - # Проксируем запрос к n8n - async with httpx.AsyncClient(timeout=60.0) as client: - response = await client.post( - N8N_FORM_APPROVE_WEBHOOK, - json=n8n_payload, - headers={"Content-Type": "application/json"}, - ) - - text = response.text or "" - - if response.status_code == 200: - logger.info( - "✅ TicketForm approval webhook OK", - extra={"response_preview": text[:500]}, - ) - try: - return json.loads(text) - except Exception: - return { - "success": True, - "message": "Form approval processed (non-JSON response from n8n)", - "raw": text, - } - - logger.error( - "❌ TicketForm approval webhook error", + + # Публикуем в Redis канал form_approve:{claim_id} + channel = f"form_approve:{claim_id}" + event_json = json.dumps(event_data, ensure_ascii=False) + await redis_service.publish(channel, event_json) + + logger.info( + f"📢 Form approval published to {channel}", extra={ - "status_code": response.status_code, - "body": text[:500], + "claim_id": claim_id, + "idempotency_key": idempotency_key, }, ) - raise HTTPException( - status_code=response.status_code, - detail=f"n8n error: {text}", - ) - - except httpx.TimeoutException: - logger.error("⏱️ n8n approval webhook timeout") - raise HTTPException(status_code=504, detail="Таймаут подключения к n8n") + + return { + "success": True, + "channel": channel, + "idempotency_key": idempotency_key, + "message": "Данные формы отправлены на обработку", + } + + except HTTPException: + raise except Exception as e: - logger.exception("❌ Ошибка при сохранении подтвержденной формы") + logger.exception("❌ Failed to publish form approval") raise HTTPException( status_code=500, - detail=f"Ошибка при сохранении подтвержденной формы: {str(e)}", + detail=f"Ошибка при отправке данных формы: {str(e)}", ) diff --git a/docs/REDIS_FORM_APPROVE.md b/docs/REDIS_FORM_APPROVE.md new file mode 100644 index 0000000..66e482a --- /dev/null +++ b/docs/REDIS_FORM_APPROVE.md @@ -0,0 +1,164 @@ +# Redis канал для подтверждения формы (form_approve) + +## 📋 Описание + +После SMS-апрува данные подтвержденной формы публикуются в Redis канал `form_approve:{claim_id}` для обработки в n8n workflow. + +## 🔄 Архитектура + +``` +Frontend (StepClaimConfirmation) + → POST /api/v1/claims/approve + → Backend публикует в Redis канал form_approve:{claim_id} + → n8n подписывается на канал и обрабатывает данные +``` + +## 📡 Endpoint + +**POST** `/api/v1/claims/approve` + +### Request Body + +```json +{ + "claim_id": "0eb051ec-23a6-4e06-8b98-f02d20d35f68", + "session_token": "sess_c9e7c0c2-de2e-40cd-ab7c-3bdc40282d34", + "session_id": "sess_c9e7c0c2-de2e-40cd-ab7c-3bdc40282d34", + "unified_id": "usr_90599ff2-ac79-4236-b950-0df85395096c", + "user_id": "user_123", + "phone": "79262306381", + "form_data": { + "user": {...}, + "project": {...}, + "offenders": [...], + "meta": {...} + }, + "user": {...}, + "project": {...}, + "offenders": [...], + "meta": {...}, + "original_data": {...} +} +``` + +### Response + +```json +{ + "success": true, + "channel": "form_approve:0eb051ec-23a6-4e06-8b98-f02d20d35f68", + "idempotency_key": "0eb051ec-23a6-4e06-8b98-f02d20d35f68_1735123456789_user_123", + "message": "Данные формы отправлены на обработку" +} +``` + +## 📢 Redis канал + +**Канал:** `form_approve:{claim_id}` + +**Формат сообщения:** + +```json +{ + "event_type": "form_approve", + "status": "approved", + "message": "Форма подтверждена после SMS-верификации", + "claim_id": "0eb051ec-23a6-4e06-8b98-f02d20d35f68", + "session_token": "sess_c9e7c0c2-de2e-40cd-ab7c-3bdc40282d34", + "unified_id": "usr_90599ff2-ac79-4236-b950-0df85395096c", + "phone": "79262306381", + "sms_verified": true, + "idempotency_key": "0eb051ec-23a6-4e06-8b98-f02d20d35f68_1735123456789_user_123", + "timestamp": "2025-11-24T14:15:26.03297+03:00", + "form_data": {...}, + "user": {...}, + "project": {...}, + "offenders": [...], + "meta": {...}, + "original_data": {...} +} +``` + +## 🔐 Idempotency Key + +Для защиты от дублей генерируется `idempotency_key`: +``` +{claim_id}_{timestamp_ms}_{user_id} +``` + +Этот ключ можно использовать в будущем для интеграции с RabbitMQ: +- Проверка дублей перед обработкой +- Дедупликация в очереди +- Гарантия идемпотентности + +## 🚀 Настройка n8n + +### 1. Redis Subscribe Node + +**Operation:** `Subscribe` +**Channel:** `form_approve:{{ $json.claim_id }}` +**Или:** Подписка на паттерн `form_approve:*` + +### 2. Обработка события + +После получения события из Redis: +1. Проверить `idempotency_key` (для защиты от дублей) +2. Обработать данные формы +3. Сохранить в БД через SQL запрос +4. Отправить уведомления (если нужно) + +### 3. Пример workflow + +``` +[Redis Subscribe] → [Check Idempotency] → [Process Form Data] → [Save to DB] → [Send Notifications] +``` + +## 🔮 Будущая интеграция с RabbitMQ + +При необходимости можно подключить RabbitMQ для: +- **Очереди:** Гарантированная обработка всех событий +- **Защита от дублей:** Проверка `idempotency_key` перед добавлением в очередь +- **Retry механизм:** Автоматические повторы при ошибках +- **Масштабирование:** Несколько воркеров для обработки + +### Структура для RabbitMQ + +```json +{ + "queue": "form_approve", + "message": { + "idempotency_key": "...", + "claim_id": "...", + "data": {...} + }, + "headers": { + "idempotency-key": "...", + "retry-count": 0 + } +} +``` + +## 📊 Мониторинг + +### Проверка канала в Redis + +```bash +redis-cli -h crm.clientright.ru -p 6379 -a 'CRM_Redis_Pass_2025_Secure!' \ + MONITOR | grep form_approve +``` + +### Подписка на канал (тест) + +```bash +redis-cli -h crm.clientright.ru -p 6379 -a 'CRM_Redis_Pass_2025_Secure!' \ + SUBSCRIBE form_approve:* +``` + +## ✅ Преимущества + +1. **Высокая производительность:** Redis Pub/Sub очень быстрый +2. **Не блокирует фронтенд:** Fire-and-forget подход +3. **Масштабируемость:** Можно добавить несколько подписчиков +4. **Готовность к RabbitMQ:** Idempotency key уже включен +5. **Простота отладки:** Можно мониторить через Redis MONITOR + diff --git a/frontend/src/components/form/StepClaimConfirmation.tsx b/frontend/src/components/form/StepClaimConfirmation.tsx index 833bd84..4ce45e5 100644 --- a/frontend/src/components/form/StepClaimConfirmation.tsx +++ b/frontend/src/components/form/StepClaimConfirmation.tsx @@ -92,28 +92,27 @@ export default function StepClaimConfirmation({ setLoading(false); }, [claimPlanData]); - // Функция сохранения данных формы - отправка в webhook без ожидания ответа + // Функция сохранения данных формы - публикация в Redis канал const saveFormData = useCallback(async (formData: any) => { - console.log('💾 Отправляем данные формы в webhook:', formData); + console.log('💾 Публикуем данные формы в Redis канал:', formData); // Получаем данные из claimPlanData для формирования payload const claimId = claimPlanData?.claim_id || claimPlanData?.propertyName?.meta?.claim_id || ''; const unifiedId = claimPlanData?.unified_id || claimPlanData?.propertyName?.meta?.unified_id || ''; const sessionToken = claimPlanData?.session_token || ''; + const userId = claimPlanData?.user_id || claimPlanData?.propertyName?.meta?.user_id || ''; const phone = claimPlanData?.propertyName?.applicant?.phone || claimPlanData?.propertyName?.user?.mobile || claimPlanData?.phone || ''; - // Формируем payload для webhook + // Формируем payload для Redis канала const payload = { - stage: 'form_approve', - form_id: 'ticket_form', - session_id: sessionToken, - session_token: sessionToken, claim_id: claimId, + session_token: sessionToken, + session_id: sessionToken, unified_id: unifiedId, + user_id: userId, phone: phone, - sms_verified: true, // Флаг что SMS код подтвержден // Данные формы подтверждения form_data: formData, @@ -126,9 +125,9 @@ export default function StepClaimConfirmation({ original_data: formData?.originalData || {}, }; - // Отправляем в webhook без ожидания ответа (fire-and-forget) - // Используем fetch с keepalive для надежности, но не ждем ответа - fetch('https://n8n.clientright.pro/webhook/eebe58d4-0bcd-4d09-9d62-39868b110960', { + // Публикуем в Redis канал через backend endpoint (fire-and-forget) + // Канал: form_approve:{claim_id} + fetch('/api/v1/claims/approve', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -137,10 +136,10 @@ export default function StepClaimConfirmation({ keepalive: true, // Продолжить отправку даже если страница закрывается }).catch((error) => { // Тихо логируем ошибки, но не блокируем пользователя - console.error('Ошибка отправки данных формы в webhook:', error); + console.error('Ошибка публикации данных формы в Redis:', error); }); - console.log('✅ Данные формы отправлены в webhook (fire-and-forget)'); + console.log('✅ Данные формы опубликованы в Redis канал form_approve:', claimId); }, [claimPlanData]); // Функция отправки SMS-кода