feat(ticket_form): unified_id/contact_id передача, исправлен мерж сессии, новая сессия для жалобы

- Добавлены unified_id и contact_id в TicketFormDescriptionRequest
- Исправлен CODE_MERGE_PROJECT_TO_SESSION.js - теперь сохраняются ВСЕ данные из body.other
- Добавлен fallback на получение other из Webhook напрямую
- Генерация новой session_id при создании новой жалобы (сохраняя авторизацию)
- Добавлен SQL_SELECT_CONTACT_WITH_CUSTOM_FIELDS.sql для CRM контактов
- Создан SESSION_LOG_2025-11-25.md с документацией сессии
This commit is contained in:
Fedor
2025-11-25 20:02:21 +03:00
parent a20a4d0e09
commit 52fe013375
39 changed files with 4155 additions and 21 deletions

View File

@@ -201,6 +201,8 @@ async def list_drafts(
c.updated_at
FROM clpr_claims c
WHERE c.unified_id = $1
AND (c.status_code != 'approved' OR c.status_code IS NULL)
AND (c.is_confirmed IS NULL OR c.is_confirmed = false)
ORDER BY c.updated_at DESC
LIMIT 20
"""
@@ -227,6 +229,8 @@ async def list_drafts(
AND ua.channel_user_id = $1
LIMIT 1
)
AND (c.status_code != 'approved' OR c.status_code IS NULL)
AND (c.is_confirmed IS NULL OR c.is_confirmed = false)
ORDER BY c.updated_at DESC
LIMIT 20
"""
@@ -246,6 +250,8 @@ async def list_drafts(
c.updated_at
FROM clpr_claims c
WHERE c.session_token = $1
AND (c.status_code != 'approved' OR c.status_code IS NULL)
AND (c.is_confirmed IS NULL OR c.is_confirmed = false)
ORDER BY c.updated_at DESC
LIMIT 20
"""
@@ -350,6 +356,7 @@ async def get_draft(claim_id: str):
# Ищем черновик по claim_id (может быть в payload->>'claim_id' или id = UUID)
# Убираем фильтры по channel и status_code, чтобы находить черновики из всех каналов
# ✅ Сортируем по updated_at DESC, чтобы получить самую свежую запись (которая может иметь send_to_form_approve)
query = """
SELECT
id,
@@ -362,6 +369,7 @@ async def get_draft(claim_id: str):
updated_at
FROM clpr_claims
WHERE (payload->>'claim_id' = $1 OR id::text = $1)
ORDER BY updated_at DESC
LIMIT 1
"""
@@ -449,6 +457,133 @@ async def delete_draft(claim_id: str):
raise HTTPException(status_code=500, detail=f"Ошибка при удалении черновика: {str(e)}")
@router.post("/approve")
async def publish_form_approval(request: Request):
"""
Публикация данных подтвержденной формы в Redis канал
После SMS-апрува отправляет данные формы в Redis канал clientright:webform:approve
для обработки в n8n workflow.
В будущем можно подключить RabbitMQ для очереди и защиты от дублей.
"""
try:
body = await request.json()
# Детальное логирование всего body для отладки
logger.info(
f"📥 Получен запрос на публикацию формы подтверждения",
extra={
"body_keys": list(body.keys()) if isinstance(body, dict) else "not_dict",
"body_type": type(body).__name__,
"sms_code_in_body": "sms_code" in body if isinstance(body, dict) else False,
"sms_code_value": body.get("sms_code", "NOT_FOUND") if isinstance(body, dict) else "NOT_DICT",
},
)
claim_id = body.get("claim_id")
session_token = body.get("session_token") or body.get("session_id")
sms_code = body.get("sms_code", "")
# Логируем полученные данные для отладки
logger.info(
f"📥 Извлеченные данные из запроса",
extra={
"claim_id": claim_id,
"sms_code": sms_code if sms_code else "(пусто)",
"sms_code_length": len(sms_code) if sms_code else 0,
"has_sms_code": bool(sms_code),
},
)
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_code": sms_code, # SMS код для верификации
"sms_verified": True,
"idempotency_key": idempotency_key, # Для защиты от дублей в RabbitMQ
"timestamp": datetime.utcnow().isoformat(),
# Данные формы подтверждения
"form_data": body.get("form_data", {}),
"user": body.get("user", {}),
"project": body.get("project", {}),
"offenders": body.get("offenders", []),
"meta": body.get("meta", {}),
# Оригинальные данные для сравнения
"original_data": body.get("original_data", {}),
}
# Публикуем в Redis канал clientright:webform:approve
channel = "clientright:webform:approve"
# Логируем event_data перед сериализацией
logger.info(
f"📢 Формируем событие для Redis канала {channel}",
extra={
"claim_id": claim_id,
"idempotency_key": idempotency_key,
"sms_code": sms_code if sms_code else "(пусто)",
"has_sms_code": bool(sms_code),
"sms_code_in_event_data": "sms_code" in event_data,
"event_data_sms_code_value": event_data.get("sms_code", "NOT_FOUND"),
"event_data_keys": list(event_data.keys()),
},
)
event_json = json.dumps(event_data, ensure_ascii=False)
# Логируем после сериализации
logger.info(
f"📢 JSON для публикации готов",
extra={
"json_length": len(event_json),
"sms_code_in_json": '"sms_code"' in event_json,
},
)
await redis_service.publish(channel, event_json)
logger.info(
f"✅ Form approval published to {channel}",
extra={
"claim_id": claim_id,
"idempotency_key": idempotency_key,
"sms_code_included": bool(sms_code),
},
)
return {
"success": True,
"channel": channel,
"idempotency_key": idempotency_key,
"message": "Данные формы отправлены на обработку",
}
except HTTPException:
raise
except Exception as e:
logger.exception("❌ Failed to publish form approval")
raise HTTPException(
status_code=500,
detail=f"Ошибка при отправке данных формы: {str(e)}",
)
@router.get("/{claim_id}")
async def get_claim(claim_id: str):
"""Получить информацию о заявке по ID"""
@@ -553,18 +688,64 @@ async def publish_ticket_form_description(payload: TicketFormDescriptionRequest)
"claim_id": payload.claim_id, # Опционально - может быть None
"phone": payload.phone,
"email": payload.email,
"unified_id": payload.unified_id, # ✅ Unified ID пользователя
"contact_id": payload.contact_id, # ✅ Contact ID пользователя
"description": payload.problem_description.strip(),
"source": payload.source,
"timestamp": datetime.utcnow().isoformat(),
}
event_json = json.dumps(event, ensure_ascii=False)
logger.info(
"📝 TicketForm description received",
extra={"session_id": payload.session_id, "claim_id": payload.claim_id or "not_set"},
extra={
"session_id": payload.session_id,
"claim_id": payload.claim_id or "not_set",
"phone": payload.phone,
"unified_id": payload.unified_id or "not_set",
"contact_id": payload.contact_id or "not_set",
"description_length": len(payload.problem_description),
"channel": channel,
},
)
await redis_service.publish(channel, json.dumps(event, ensure_ascii=False))
logger.info(
"📡 TicketForm description published",
extra={"channel": channel, "session_id": payload.session_id},
"📡 Publishing to Redis channel",
extra={
"channel": channel,
"event_type": event["type"],
"event_keys": list(event.keys()),
"json_length": len(event_json),
},
)
subscribers_count = await redis_service.publish(channel, event_json)
logger.info(
"✅ TicketForm description published to Redis",
extra={
"channel": channel,
"session_id": payload.session_id,
"subscribers_count": subscribers_count,
"event_json_preview": event_json[:500],
},
)
if subscribers_count == 0:
logger.warning(
f"⚠️ WARNING: No subscribers on channel {channel}! "
f"n8n workflow is not listening to this channel. "
f"Event was published but will be lost."
)
# Дополнительная проверка: логируем полный event для отладки
logger.debug(
"🔍 Full event data published",
extra={
"channel": channel,
"event": event,
},
)
return {
"success": True,