feat: Получение cf_2624 из MySQL и блокировка полей при подтверждении данных

- Добавлен сервис CrmMySQLService для прямого подключения к MySQL CRM
- Обновлён метод get_draft() для получения cf_2624 напрямую из БД
- Реализована блокировка полей (readonly) при contact_data_confirmed = true
- Добавлен выбор банка для СБП выплат с динамической загрузкой из API
- Обновлена документация по работе с cf_2624 и MySQL
- Добавлен network_mode: host в docker-compose для доступа к MySQL
- Обновлены компоненты формы для поддержки блокировки полей
This commit is contained in:
AI Assistant
2025-12-04 12:22:23 +03:00
parent 64385c430d
commit 080e7ec105
69 changed files with 17034 additions and 1439 deletions

View File

@@ -15,6 +15,7 @@ import json
import logging
from ..services.redis_service import redis_service
from ..services.database import db
from ..services.crm_mysql_service import crm_mysql_service
from ..config import settings
router = APIRouter(prefix="/api/v1/claims", tags=["Claims"])
@@ -201,17 +202,19 @@ 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)
-- ВРЕМЕННО: убираем все фильтры для диагностики
-- TODO: вернуть фильтры после выяснения проблемы
-- AND (c.is_confirmed IS NULL OR c.is_confirmed = false)
ORDER BY c.updated_at DESC
LIMIT 20
"""
params = [unified_id]
logger.info(f"🔍 Searching by unified_id: {unified_id}")
elif phone:
# Fallback: ищем через clpr_user_accounts и clpr_users
# Fallback: ищем через clpr_user_accounts и clpr_users, ИЛИ напрямую по телефону в payload
# Поддерживаем разные форматы телефона: 71234543212, +71234543212, 81234543212
query = """
SELECT
SELECT DISTINCT
c.id,
c.payload->>'claim_id' as claim_id,
c.session_token,
@@ -221,21 +224,35 @@ async def list_drafts(
c.created_at,
c.updated_at
FROM clpr_claims c
WHERE c.unified_id = (
SELECT u.unified_id
FROM clpr_user_accounts ua
JOIN clpr_users u ON u.id = ua.user_id
WHERE ua.channel = 'web_form'
AND ua.channel_user_id = $1
LIMIT 1
)
WHERE c.channel = 'web_form'
AND (
-- Вариант 1: Поиск через unified_id (если есть запись в clpr_user_accounts)
c.unified_id = (
SELECT u.unified_id
FROM clpr_user_accounts ua
JOIN clpr_users u ON u.id = ua.user_id
WHERE ua.channel = 'web_form'
AND (ua.channel_user_id = $1 OR ua.channel_user_id = $2 OR ua.channel_user_id = $3)
LIMIT 1
)
-- Вариант 2: Прямой поиск по телефону в payload (в разных форматах)
OR c.payload->>'phone' = $1
OR c.payload->>'phone' = $2
OR c.payload->>'phone' = $3
)
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
"""
params = [phone]
logger.info(f"🔍 Searching by phone (fallback): {phone}")
# Подготавливаем варианты телефона для поиска
phone_variants = [
phone, # Оригинальный формат
f"+{phone}", # С плюсом
phone.replace('7', '8', 1) if phone.startswith('7') else phone # С 8 вместо 7
]
params = phone_variants
logger.info(f"🔍 Searching by phone (fallback): {phone}, variants: {phone_variants}")
elif session_id:
# Fallback: поиск по session_token
query = """
@@ -264,9 +281,22 @@ async def list_drafts(
# Простой тест: проверяем, что unified_id вообще есть в базе
test_count = 0
test_count_null = 0
test_count_approved = 0
test_count_confirmed = 0
if unified_id:
try:
# Все заявления с этим unified_id
test_count = await db.fetch_val("SELECT COUNT(*) FROM clpr_claims WHERE unified_id = $1", unified_id)
# Заявления со статусом approved
test_count_approved = await db.fetch_val("""
SELECT COUNT(*) FROM clpr_claims
WHERE unified_id = $1 AND status_code = 'approved'
""", unified_id)
# Заявления с is_confirmed = true
test_count_confirmed = await db.fetch_val("""
SELECT COUNT(*) FROM clpr_claims
WHERE unified_id = $1 AND is_confirmed = true
""", unified_id)
# Также проверяем, сколько записей с NULL unified_id для этого пользователя (через phone)
if phone:
test_count_null = await db.fetch_val("""
@@ -275,7 +305,7 @@ async def list_drafts(
AND c.channel = 'web_form'
AND c.payload->>'phone' = $1
""", phone)
logger.info(f"🔍 Test COUNT: unified_id={unified_id}{test_count} records")
logger.info(f"🔍 Test COUNT: unified_id={unified_id}{test_count} total, {test_count_approved} approved, {test_count_confirmed} confirmed")
if test_count_null > 0:
logger.warning(f"⚠️ Found {test_count_null} records with NULL unified_id for phone={phone}")
except Exception as e:
@@ -290,10 +320,25 @@ async def list_drafts(
logger.info(f"🔍 Test COUNT result: {test_count}")
logger.info(f"🔍 Rows found: {len(rows)}")
# Если заявления есть, но не возвращаются - проверяем статусы
if len(rows) == 0 and test_count > 0 and unified_id:
logger.warning(f"⚠️ Заявления есть (test_count={test_count}), но запрос вернул 0 строк!")
try:
all_statuses = await db.fetch_all("""
SELECT status_code, is_confirmed, channel, id
FROM clpr_claims
WHERE unified_id = $1
""", unified_id)
logger.warning(f"⚠️ Все заявления для unified_id: {[dict(r) for r in all_statuses]}")
except Exception as e:
logger.error(f"❌ Ошибка при проверке статусов: {e}")
# ВРЕМЕННО: возвращаем тестовые данные для отладки
debug_info = {
"unified_id": unified_id,
"test_count": test_count,
"test_count_approved": test_count_approved or 0,
"test_count_confirmed": test_count_confirmed or 0,
"test_count_null": test_count_null,
"rows_found": len(rows),
"query": query[:200] if len(query) > 200 else query,
@@ -316,18 +361,68 @@ async def list_drafts(
else:
payload = {}
# Извлекаем данные из ai_analysis или wizard_plan
ai_analysis = payload.get('ai_analysis') or {}
wizard_plan = payload.get('wizard_plan') or {}
# Краткое описание проблемы (заголовок)
problem_title = ai_analysis.get('problem') or payload.get('problem') or None
# Категория проблемы
category = ai_analysis.get('category') or wizard_plan.get('category') or None
# Подробное описание (для превью)
problem_text = payload.get('problem_description', '')
# Считаем документы
documents_meta = payload.get('documents_meta') or []
documents_required = payload.get('documents_required') or []
# Считаем загруженные (уникальные по field_label)
uploaded_labels = set()
for doc in documents_meta:
label = doc.get('field_label') or doc.get('field_name')
if label:
uploaded_labels.add(label)
documents_uploaded = len(uploaded_labels)
documents_total = len(documents_required) if documents_required else 0
# Формируем список документов со статусами
documents_list = []
for doc_req in documents_required:
doc_name = doc_req.get('name', 'Документ')
doc_id = doc_req.get('id', '')
is_required = doc_req.get('required', False)
# Проверяем загружен ли (по name или id)
is_uploaded = doc_name in uploaded_labels or doc_id in uploaded_labels
documents_list.append({
"name": doc_name,
"required": is_required,
"uploaded": is_uploaded,
})
drafts.append({
"id": str(row['id']),
"claim_id": row.get('claim_id'),
"session_token": row.get('session_token'),
"status_code": row.get('status_code'),
"channel": row.get('channel'), # Добавляем канал в ответ
"channel": row.get('channel'),
"created_at": row['created_at'].isoformat() if row.get('created_at') else None,
"updated_at": row['updated_at'].isoformat() if row.get('updated_at') else None,
"problem_description": payload.get('problem_description', '')[:100] if payload.get('problem_description') else None,
# Заголовок - краткое описание проблемы из AI
"problem_title": problem_title[:150] if problem_title else None,
# Полное описание
"problem_description": problem_text[:500] if problem_text else None,
"category": category,
"wizard_plan": payload.get('wizard_plan') is not None,
"wizard_answers": payload.get('answers') is not None,
"has_documents": len(payload.get('documents_meta', [])) > 0 if payload.get('documents_meta') else False,
"has_documents": documents_uploaded > 0,
# Прогресс документов
"documents_total": documents_total,
"documents_uploaded": documents_uploaded,
"documents_skipped": 0, # TODO: считать пропущенные
"documents_list": documents_list, # Список со статусами
})
return {
@@ -406,18 +501,114 @@ async def get_draft(claim_id: str):
if documents_required:
logger.info(f"🔍 documents_required: {documents_required[:2]}...") # Первые 2 для примера
# ✅ Проверяем флаг подтверждения данных контакта из CRM (поле cf_2624)
# Простой способ: делаем прямой SQL запрос к БД (таблицы vtiger_*)
# ПРИМЕЧАНИЕ: Если таблицы vtiger_* находятся в MySQL (а не PostgreSQL),
# нужно использовать отдельный connection через policy_service или создать новый MySQL connection
unified_id = row.get('unified_id')
contact_data_confirmed = False
contact_data_can_edit = True
contact_data_from_crm = None
# Получаем contact_id из payload
contact_id = payload.get('contact_id') if isinstance(payload, dict) else None
# Преобразуем contact_id в строку, если он есть
if contact_id:
contact_id = str(contact_id).strip()
logger.info(f"🔍 Получен contact_id из черновика: {contact_id} (type: {type(contact_id)})")
if contact_id:
try:
# ✅ Прямой SQL запрос к MySQL для получения cf_2624
# Таблицы vtiger_* находятся в MySQL БД
contact_query = """
SELECT
cd.contactid,
cd.firstname,
cd.lastname,
cd.email,
cd.mobile,
cd.phone,
cs.birthday,
ca.mailingstreet,
ca.mailingcity,
ca.mailingstate,
ca.mailingzip,
ca.mailingcountry,
ccf.cf_1157 AS middle_name,
ccf.cf_1263 AS birthplace,
ccf.cf_1257 AS inn,
ccf.cf_1849 AS requisites,
ccf.cf_1580 AS code,
ccf.cf_1706 AS sms,
ccf.cf_2624 AS cf_2624
FROM vtiger_contactdetails cd
LEFT JOIN vtiger_contactscf ccf ON ccf.contactid = cd.contactid
LEFT JOIN vtiger_contactsubdetails cs ON cs.contactsubscriptionid = cd.contactid
LEFT JOIN vtiger_contactaddress ca ON ca.contactaddressid = cd.contactid
LEFT JOIN vtiger_crmentity ce ON ce.crmid = cd.contactid
WHERE cd.contactid = %s
AND ce.deleted = 0
LIMIT 1
"""
contact_row = await crm_mysql_service.fetch_one(contact_query, contact_id)
if contact_row:
# Формируем объект с данными контакта
contact_data_from_crm = {
"contactid": contact_row.get("contactid"),
"firstname": contact_row.get("firstname"),
"lastname": contact_row.get("lastname"),
"email": contact_row.get("email"),
"mobile": contact_row.get("mobile"),
"phone": contact_row.get("phone"),
"birthday": contact_row.get("birthday"),
"mailingstreet": contact_row.get("mailingstreet"),
"mailingcity": contact_row.get("mailingcity"),
"mailingstate": contact_row.get("mailingstate"),
"mailingzip": contact_row.get("mailingzip"),
"mailingcountry": contact_row.get("mailingcountry"),
"cf_1157": contact_row.get("middle_name"), # Отчество
"cf_1263": contact_row.get("birthplace"), # Место рождения
"cf_1257": contact_row.get("inn"), # ИНН
"cf_1849": contact_row.get("requisites"), # Реквизиты
"cf_1580": contact_row.get("code"), # Код
"cf_1706": contact_row.get("sms"), # SMS
"cf_2624": contact_row.get("cf_2624") or "0" # ✅ Данные подтверждены
}
# ✅ Проверяем кастомное поле "Данные подтверждены" (cf_2624)
confirmed_field = contact_data_from_crm.get("cf_2624", "0")
contact_data_confirmed = confirmed_field == "1" or confirmed_field == "true" or confirmed_field is True
contact_data_can_edit = not contact_data_confirmed
logger.info(
f"🔒 Статус данных контакта из MySQL CRM: confirmed={contact_data_confirmed}, "
f"field_value={confirmed_field}, contact_id={contact_id}"
)
else:
logger.warning(f"⚠️ Контакт не найден в MySQL CRM: contact_id={contact_id}")
except Exception as e:
logger.warning(f"⚠️ Не удалось загрузить данные контакта из MySQL CRM: {str(e)}")
return {
"success": True,
"claim": {
"id": str(row['id']),
"claim_id": final_claim_id, # ✅ Используем claim_id из payload, если его нет в row
"claim_id": final_claim_id,
"session_token": row.get('session_token'),
"status_code": row.get('status_code'),
"channel": row.get('channel'), # ✅ Добавляем channel для отладки
"channel": row.get('channel'),
"created_at": row['created_at'].isoformat() if row.get('created_at') else None,
"updated_at": row['updated_at'].isoformat() if row.get('updated_at') else None,
"payload": payload
}
},
# ✅ Флаги подтверждения данных контакта (из CRM поля cf_2624)
"contact_data_confirmed": contact_data_confirmed,
"contact_data_can_edit": contact_data_can_edit,
"contact_data_from_crm": contact_data_from_crm # Данные из CRM (всегда загружаем, если есть contact_id)
}
except HTTPException:
@@ -483,6 +674,10 @@ async def publish_form_approval(request: Request):
"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",
"contact_data_confirmed_in_body": "contact_data_confirmed" in body if isinstance(body, dict) else False,
"cf_2624_in_body": "cf_2624" in body if isinstance(body, dict) else False,
"bank_id_in_body": "bank_id" in body if isinstance(body, dict) else False,
"bank_name_in_body": "bank_name" in body if isinstance(body, dict) else False,
},
)
@@ -508,6 +703,27 @@ async def publish_form_approval(request: Request):
import time
idempotency_key = f"{claim_id}_{int(time.time() * 1000)}_{body.get('user_id', 'unknown')}"
# ✅ Получаем флаг подтверждения данных контакта и данные банка
contact_data_confirmed = body.get("contact_data_confirmed", False)
cf_2624 = body.get("cf_2624", "0")
bank_id = body.get("bank_id", "")
bank_name = body.get("bank_name", "")
# Логируем полученные значения для отладки
logger.info(
f"📥 Извлеченные дополнительные поля",
extra={
"contact_data_confirmed": contact_data_confirmed,
"cf_2624": cf_2624,
"bank_id": bank_id,
"bank_name": bank_name,
"has_contact_data_confirmed": "contact_data_confirmed" in body,
"has_cf_2624": "cf_2624" in body,
"has_bank_id": "bank_id" in body,
"has_bank_name": "bank_name" in body,
},
)
# Формируем событие для Redis
event_data = {
"event_type": "form_approve",
@@ -522,6 +738,14 @@ async def publish_form_approval(request: Request):
"idempotency_key": idempotency_key, # Для защиты от дублей в RabbitMQ
"timestamp": datetime.utcnow().isoformat(),
# ✅ Флаг редактирования перс данных (cf_2624)
"contact_data_confirmed": contact_data_confirmed,
"cf_2624": cf_2624, # Значение для CRM (1 = подтверждено, 0 = не подтверждено)
# ✅ Данные банка для СБП выплаты
"bank_id": bank_id,
"bank_name": bank_name,
# Данные формы подтверждения
"form_data": body.get("form_data", {}),
"user": body.get("user", {}),
@@ -547,6 +771,10 @@ async def publish_form_approval(request: Request):
"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()),
"contact_data_confirmed_in_event": "contact_data_confirmed" in event_data,
"cf_2624_in_event": "cf_2624" in event_data,
"bank_id_in_event": "bank_id" in event_data,
"bank_name_in_event": "bank_name" in event_data,
},
)