feat: Switch form approval to Redis channel instead of webhook
Changed from webhook to Redis Pub/Sub channel:
- Created endpoint POST /api/v1/claims/approve
- Publishes to Redis channel: form_approve:{claim_id}
- Added idempotency_key for future RabbitMQ integration
- Fire-and-forget approach (no waiting for response)
Benefits:
- Higher performance (Redis Pub/Sub is faster)
- Better scalability
- Ready for RabbitMQ queue integration
- Idempotency key included for duplicate protection
Files:
- backend/app/api/claims.py (new /approve endpoint)
- frontend/src/components/form/StepClaimConfirmation.tsx (updated saveFormData)
- docs/REDIS_FORM_APPROVE.md (documentation)
This commit is contained in:
@@ -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)}",
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user