233 lines
12 KiB
Python
233 lines
12 KiB
Python
from enum import StrEnum, auto
|
|
from typing import Dict, Optional
|
|
from loguru import logger
|
|
from core.models import LeadData
|
|
from core.validator import validate_fio, validate_phone, parse_time
|
|
from core.exporter import ExcelExporter
|
|
from config import phrases
|
|
|
|
class DialogState(StrEnum):
|
|
START = auto()
|
|
ASK_CONSENT = auto()
|
|
COLLECT_FI = auto() # ФИО пользователя
|
|
ASK_PARENT_DATA = auto() # Спрашиваем, хочет ли пользователь ввести данные родителей
|
|
COLLECT_PARENT_FIO = auto() # Ввод ФИО родителя
|
|
COLLECT_PARENT_PHONE = auto() # Ввод телефона родителя
|
|
COLLECT_PHONE = auto()
|
|
COLLECT_TIME = auto()
|
|
CONFIRM = auto()
|
|
FINISHED = auto()
|
|
OBJECTION_HANDLED = auto()
|
|
|
|
class DialogManager:
|
|
def __init__(self, exporter: ExcelExporter):
|
|
self.exporter = exporter
|
|
self.sessions: Dict[int, dict] = {}
|
|
|
|
# def _handle_age(self, user_id: int, text: str) -> str:
|
|
# result = validate_age(text)
|
|
# if not result.is_valid:
|
|
# return result.error_message + "\n" + phrases.ASK_AGE
|
|
# session = self.sessions[user_id]
|
|
# session["data"].age = result.value
|
|
# logger.info(f"User {user_id} age = {result.value}")
|
|
# if result.value < 18:
|
|
# session["state"] = DialogState.ASK_PARENT_DATA
|
|
# logger.info(f"State changed to ASK_PARENT_DATA for user {user_id}")
|
|
# return phrases.ASK_PARENT_CONSENT
|
|
# else:
|
|
# session["state"] = DialogState.COLLECT_PHONE
|
|
# logger.info(f"Age >=18, state changed to COLLECT_PHONE")
|
|
# return phrases.ASK_PHONE
|
|
|
|
def _handle_parent_consent(self, user_id: int, text: str) -> str:
|
|
"""Обрабатывает ответ на вопрос про данные родителей.
|
|
Если пользователь вводит 'пропустить' и т.п. — пропускает.
|
|
Иначе считает, что это и есть ФИО родителя, и переходит к телефону.
|
|
"""
|
|
session = self.sessions[user_id]
|
|
text_lower = text.lower().strip()
|
|
if text_lower in ("пропустить", "нет", "не надо", "skip", "не хочу"):
|
|
session["state"] = DialogState.COLLECT_PHONE
|
|
logger.info(f"Parent data skipped for user {user_id}")
|
|
return phrases.PARENT_DATA_SKIPPED + "\n" + phrases.ASK_PHONE
|
|
else:
|
|
# Считаем, что пользователь сразу ввёл ФИО родителя
|
|
result = validate_fio(text)
|
|
if not result.is_valid:
|
|
return phrases.PARENT_FIO_INVALID + "\n" + phrases.ASK_PARENT_CONSENT
|
|
session["data"].parent_full_name = result.value
|
|
session["state"] = DialogState.COLLECT_PARENT_PHONE
|
|
logger.info(f"Parent FIO saved for user {user_id}, state changed to COLLECT_PARENT_PHONE")
|
|
return phrases.ASK_PARENT_PHONE
|
|
|
|
|
|
def _handle_parent_fio(self, user_id: int, text: str) -> str:
|
|
result = validate_fio(text) # используем ту же валидацию, что и для ФИО пользователя
|
|
if not result.is_valid:
|
|
# Подменяем сообщение об ошибке для родителя
|
|
return phrases.PARENT_FIO_INVALID + "\n" + phrases.ASK_PARENT_FIO
|
|
session = self.sessions[user_id]
|
|
session["data"].parent_full_name = result.value
|
|
session["state"] = DialogState.COLLECT_PARENT_PHONE
|
|
return phrases.ASK_PARENT_PHONE
|
|
|
|
def _handle_parent_phone(self, user_id: int, text: str) -> str:
|
|
result = validate_phone(text) # используем существующий валидатор телефона
|
|
if not result.is_valid:
|
|
return phrases.PARENT_PHONE_INVALID + "\n" + phrases.ASK_PARENT_PHONE
|
|
session = self.sessions[user_id]
|
|
session["data"].parent_phone = result.value
|
|
session["state"] = DialogState.COLLECT_PHONE
|
|
return phrases.PARENT_DATA_SAVED + "\n" + phrases.ASK_PHONE
|
|
|
|
def handle_message(self, user_id: int, text: str) -> str:
|
|
"""Обрабатывает сообщение и возвращает ответ бота"""
|
|
logger.info(f"=== HANDLE: user {user_id}, text='{text}', current state={self.sessions.get(user_id, {}).get('state')} ===")
|
|
# Команда /start или "начать заново"
|
|
if text.lower() in (phrases.CMD_START, phrases.BUTTON_RESTART.lower(), "начать заново"):
|
|
return self._reset_dialog(user_id)
|
|
|
|
session = self.sessions.get(user_id)
|
|
if not session:
|
|
return self._start_dialog(user_id)
|
|
|
|
state = session["state"]
|
|
lead = session["data"]
|
|
|
|
# Обработка возражений на любом этапе
|
|
if self._is_objection(text):
|
|
return self._handle_objection(user_id, text)
|
|
|
|
# Продолжение после возражения "подумаю"
|
|
if state == DialogState.OBJECTION_HANDLED and text.lower() == phrases.BUTTON_CONTINUE.lower():
|
|
# Возвращаемся к предыдущему состоянию
|
|
prev_state = session.get("prev_state", DialogState.COLLECT_FI)
|
|
session["state"] = prev_state
|
|
# Не рекурсивный вызов, а продолжение основного потока
|
|
state = session["state"]
|
|
lead = session["data"]
|
|
|
|
# Обычные состояния
|
|
if state == DialogState.ASK_CONSENT:
|
|
return self._handle_consent(user_id, text)
|
|
if state == DialogState.COLLECT_FI:
|
|
return self._handle_fio(user_id, text)
|
|
elif state == DialogState.ASK_PARENT_DATA: # Спрашиваем про данные родителей
|
|
return self._handle_parent_consent(user_id, text)
|
|
elif state == DialogState.COLLECT_PARENT_FIO:
|
|
return self._handle_parent_fio(user_id, text)
|
|
elif state == DialogState.COLLECT_PARENT_PHONE:
|
|
return self._handle_parent_phone(user_id, text)
|
|
elif state == DialogState.COLLECT_PHONE:
|
|
return self._handle_phone(user_id, text)
|
|
elif state == DialogState.COLLECT_TIME:
|
|
return self._handle_time(user_id, text)
|
|
elif state == DialogState.CONFIRM:
|
|
return self._handle_confirm(user_id, text)
|
|
|
|
def _start_dialog(self, user_id: int) -> str:
|
|
self.sessions[user_id] = {
|
|
"state": DialogState.ASK_CONSENT, # ← было COLLECT_FI
|
|
"data": LeadData(user_id=user_id)
|
|
}
|
|
return phrases.ASK_CONSENT
|
|
|
|
def _reset_dialog(self, user_id: int) -> str:
|
|
if user_id in self.sessions:
|
|
del self.sessions[user_id]
|
|
return self._start_dialog(user_id)
|
|
|
|
def _handle_fio(self, user_id: int, text: str) -> str:
|
|
result = validate_fio(text)
|
|
if not result.is_valid:
|
|
return result.error_message + "\n" + phrases.ASK_FI
|
|
session = self.sessions[user_id]
|
|
session["data"].full_name = result.value
|
|
session["state"] = DialogState.ASK_PARENT_DATA
|
|
return phrases.ASK_PARENT_CONSENT
|
|
|
|
def _handle_phone(self, user_id: int, text: str) -> str:
|
|
result = validate_phone(text)
|
|
if not result.is_valid:
|
|
return result.error_message + "\n" + phrases.ASK_PHONE
|
|
session = self.sessions[user_id]
|
|
session["data"].phone = result.value
|
|
session["state"] = DialogState.COLLECT_TIME
|
|
return phrases.ASK_TIME
|
|
|
|
def _handle_time(self, user_id: int, text: str) -> str:
|
|
result = parse_time(text)
|
|
if not result.is_valid:
|
|
return result.error_message + "\n" + phrases.ASK_TIME
|
|
session = self.sessions[user_id]
|
|
session["data"].preferred_time = result.value
|
|
parent_info = ""
|
|
if session["data"].parent_full_name and session["data"].parent_phone:
|
|
parent_info = f"Родитель: {session['data'].parent_full_name}, тел: {session['data'].parent_phone}\n"
|
|
elif session["data"].parent_full_name:
|
|
parent_info = f"Родитель: {session['data'].parent_full_name} (телефон не указан)\n"
|
|
elif session["data"].parent_phone:
|
|
parent_info = f"Родитель: телефон {session['data'].parent_phone} (ФИО не указано)\n"
|
|
session["state"] = DialogState.CONFIRM
|
|
return phrases.CONFIRM_MESSAGE.format(
|
|
fio=session["data"].full_name,
|
|
phone=session["data"].phone,
|
|
time=session["data"].preferred_time,
|
|
parent_info=parent_info
|
|
)
|
|
|
|
def _handle_confirm(self, user_id: int, text: str) -> str:
|
|
if text.lower() in ("да", phrases.BUTTON_YES.lower()):
|
|
lead = self.sessions[user_id]["data"]
|
|
lead.status = "completed"
|
|
self.exporter.save_lead(lead)
|
|
self.sessions[user_id]["state"] = DialogState.FINISHED
|
|
return phrases.CONFIRM_YES
|
|
elif text.lower() in ("нет", phrases.BUTTON_NO.lower()):
|
|
return self._reset_dialog(user_id)
|
|
else:
|
|
return "Пожалуйста, ответьте 'Да' или 'Нет'."
|
|
|
|
def _is_objection(self, text: str) -> bool:
|
|
obj_phrases = ["не сейчас", "подумаю", "не хочу оставлять телефон", "не хочу говорить номер"]
|
|
return any(phrase in text.lower() for phrase in obj_phrases)
|
|
|
|
def _handle_objection(self, user_id: int, text: str) -> str:
|
|
session = self.sessions.get(user_id)
|
|
if not session:
|
|
return self._start_dialog(user_id)
|
|
|
|
lead = session["data"]
|
|
if "не сейчас" in text.lower():
|
|
lead.status = "postponed"
|
|
self.exporter.save_lead(lead)
|
|
self.sessions[user_id]["state"] = DialogState.FINISHED
|
|
return phrases.OBJECTION_NOT_NOW
|
|
elif "подумаю" in text.lower():
|
|
# Сохраняем текущее состояние, переходим в OBJECTION_HANDLED
|
|
session["prev_state"] = session["state"]
|
|
session["state"] = DialogState.OBJECTION_HANDLED
|
|
return phrases.OBJECTION_THINK
|
|
elif any(phrase in text.lower() for phrase in ["не хочу оставлять телефон", "не хочу говорить номер"]):
|
|
lead.status = "rejected"
|
|
self.exporter.save_lead(lead)
|
|
self.sessions[user_id]["state"] = DialogState.FINISHED
|
|
return phrases.OBJECTION_NO_PHONE
|
|
return self.handle_message(user_id, text) # fallback
|
|
|
|
def _handle_consent(self, user_id: int, text: str) -> str:
|
|
text_lower = text.lower().strip()
|
|
session = self.sessions.get(user_id)
|
|
if not session:
|
|
return self._start_dialog(user_id)
|
|
|
|
if any(phrase in text_lower for phrase in phrases.CONSENT_YES_PHRASES):
|
|
session["data"].consent_given = True
|
|
session["state"] = DialogState.COLLECT_FI # ← было COLLECT_FIO
|
|
return phrases.CONSENT_YES + "\n" + phrases.ASK_FI # ← было ASK_FIO
|
|
elif any(phrase in text_lower for phrase in phrases.CONSENT_NO_PHRASES):
|
|
del self.sessions[user_id]
|
|
return phrases.CONSENT_NO
|
|
else:
|
|
return "Пожалуйста, ответьте 'Да' (согласен) или 'Нет' (не согласен)." |