v1.6.0: max_bot fixes — feature keys, flush→commit, test-run, categories, broadcast page, proxy error handling, deploy scripts
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,105 @@
|
||||
from datetime import datetime
|
||||
from app.settings_cache import SettingsCache
|
||||
from app.conversation import get_or_create_user, get_template_rendered
|
||||
from app.fsm import BotState, FSM
|
||||
from app.keyboards.inline import build_main_menu_buttons
|
||||
|
||||
_DATE_KEY = "appt_date"
|
||||
_SLOT_KEY = "appt_slot"
|
||||
|
||||
|
||||
async def handle_appointment(bot, user_data: dict, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
text = user_data.get("text", "").strip()
|
||||
payload = user_data.get("payload", "")
|
||||
state = FSM.get_state(max_user_id)
|
||||
|
||||
if state != BotState.FEATURE_APPOINTMENT:
|
||||
FSM.set_state(max_user_id, BotState.FEATURE_APPOINTMENT)
|
||||
FSM.set_temp(max_user_id, {})
|
||||
await max_api.send_message(
|
||||
max_user_id,
|
||||
"📅 **Запись на визит**\n\nУкажите желаемую дату (ДД.ММ.ГГГГ):"
|
||||
)
|
||||
return
|
||||
|
||||
tmp = FSM.get_temp(max_user_id)
|
||||
|
||||
if _DATE_KEY not in tmp:
|
||||
try:
|
||||
parsed = datetime.strptime(text.strip(), "%d.%m.%Y")
|
||||
tmp[_DATE_KEY] = parsed.strftime("%Y-%m-%d")
|
||||
FSM.set_temp(max_user_id, tmp)
|
||||
|
||||
slots = _generate_slots()
|
||||
msg_lines = [f"📅 **{parsed.strftime('%d.%m.%Y')}**\n\nДоступные слоты:\n"]
|
||||
for i, slot in enumerate(slots, 1):
|
||||
msg_lines.append(f"{i}. {slot}")
|
||||
msg_lines.append("\nВведите номер слота (1-{}):".format(len(slots)))
|
||||
await max_api.send_message(max_user_id, "\n".join(msg_lines), format="markdown")
|
||||
except Exception:
|
||||
await max_api.send_message(max_user_id, "Неверный формат даты. Укажите дату в формате ДД.ММ.ГГГГ")
|
||||
return
|
||||
|
||||
if _SLOT_KEY not in tmp:
|
||||
slots = _generate_slots()
|
||||
try:
|
||||
idx = int(text.strip()) - 1
|
||||
if 0 <= idx < len(slots):
|
||||
tmp[_SLOT_KEY] = slots[idx]
|
||||
FSM.set_temp(max_user_id, tmp)
|
||||
await _confirm_appointment(bot, user_data, db, max_api, tmp)
|
||||
return
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
await max_api.send_message(max_user_id, f"Укажите номер слота (1-{len(slots)}):")
|
||||
return
|
||||
|
||||
await _confirm_appointment(bot, user_data, db, max_api, tmp)
|
||||
|
||||
|
||||
def _generate_slots() -> list:
|
||||
start_h = SettingsCache.get_int("work_hours_start", 9)
|
||||
end_h = SettingsCache.get_int("work_hours_end", 18)
|
||||
slots = []
|
||||
for h in range(start_h, end_h):
|
||||
slots.append(f"{h:02d}:00-{h + 1:02d}:00")
|
||||
return slots
|
||||
|
||||
|
||||
async def _confirm_appointment(bot, user_data, db, max_api, tmp):
|
||||
max_user_id = user_data["user_id"]
|
||||
date_str = tmp.get(_DATE_KEY, "—")
|
||||
slot_str = tmp.get(_SLOT_KEY, "—")
|
||||
user = await get_or_create_user(db, max_user_id)
|
||||
text = await get_template_rendered(db, "appointment_confirm",
|
||||
date=f"{date_str} {slot_str}",
|
||||
engineer="будет назначен")
|
||||
await max_api.send_message(max_user_id, text, format="markdown")
|
||||
|
||||
conv = await _create_appointment_conv(db, user.id, f"{date_str} {slot_str}")
|
||||
from app.integrations import _1c_unf as ic_integration
|
||||
await ic_integration.send_to_1c(db, conv)
|
||||
|
||||
FSM.clear_temp(max_user_id)
|
||||
FSM.transition(max_user_id, "done")
|
||||
buttons = build_main_menu_buttons()
|
||||
menu_text = await get_template_rendered(db, "main_menu")
|
||||
await max_api.send_message_with_keyboard(max_user_id, menu_text, buttons)
|
||||
|
||||
|
||||
async def _create_appointment_conv(db, user_id, details):
|
||||
from app.conversation import create_conversation, add_message
|
||||
from sqlalchemy import select
|
||||
from app.models.models import BotCategory
|
||||
result = await db.execute(
|
||||
select(BotCategory).where(BotCategory.name == "Обслуживание").limit(1)
|
||||
)
|
||||
cat = result.scalar_one_or_none()
|
||||
conv = await create_conversation(db, user_id)
|
||||
conv.inquiry_text = f"Запись на визит: {details}"
|
||||
conv.inquiry_type = cat.id if cat else None
|
||||
conv.priority = "normal"
|
||||
await db.flush()
|
||||
await add_message(db, conv.id, "in", details)
|
||||
return conv
|
||||
@@ -0,0 +1,44 @@
|
||||
from datetime import datetime
|
||||
from app.settings_cache import SettingsCache
|
||||
from app.conversation import get_or_create_user, get_template_rendered
|
||||
from app.fsm import BotState, FSM
|
||||
|
||||
|
||||
async def handle_broadcast_send(bot, user_data: dict, db, max_api):
|
||||
from app.models.models import BotUser
|
||||
from sqlalchemy import select
|
||||
|
||||
text = user_data.get("text", "").strip()
|
||||
if not text:
|
||||
await max_api.send_message(
|
||||
user_data["user_id"],
|
||||
"Укажите текст рассылки в параметре broadcast_text."
|
||||
)
|
||||
return
|
||||
|
||||
result = await db.execute(select(BotUser).where(BotUser.consent_given == True))
|
||||
users = result.scalars().all()
|
||||
sent = 0
|
||||
failed = 0
|
||||
sent_ids = []
|
||||
for u in users:
|
||||
try:
|
||||
await max_api.send_message(user_id=u.max_user_id, text=text, format="markdown")
|
||||
sent += 1
|
||||
sent_ids.append(u.max_user_id)
|
||||
except Exception:
|
||||
failed += 1
|
||||
|
||||
from app.models.models import BotBroadcast
|
||||
bc = BotBroadcast(
|
||||
text=text,
|
||||
sent_at=datetime.now(),
|
||||
recipient_count=len(users),
|
||||
recipient_ids=sent_ids,
|
||||
status="sent",
|
||||
)
|
||||
db.add(bc)
|
||||
await db.flush()
|
||||
|
||||
result = f"📨 Рассылка завершена.\nОтправлено: {sent}\nОшибок: {failed}"
|
||||
await max_api.send_message(user_data["user_id"], result)
|
||||
@@ -0,0 +1,101 @@
|
||||
from app.settings_cache import SettingsCache
|
||||
from app.conversation import get_or_create_user, get_template_rendered
|
||||
from app.fsm import BotState, FSM
|
||||
from app.keyboards.inline import build_main_menu_buttons
|
||||
|
||||
_STEPS = ["service_type", "scope", "address", "comment"]
|
||||
_STEP_LABELS = {
|
||||
"service_type": "Тип услуги (Видеонаблюдение, СКУД, Пожарная сигнализация, IT-инфраструктура, Обслуживание, Другое)",
|
||||
"scope": "Объём работ (краткое описание)",
|
||||
"address": "Адрес объекта",
|
||||
"comment": "Дополнительные пожелания (или отправьте «-» для пропуска)",
|
||||
}
|
||||
|
||||
_COLLECT_KEY = "commercial_data"
|
||||
_STEP_KEY = "commercial_step"
|
||||
|
||||
|
||||
async def handle_commercial(bot, user_data: dict, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
text = user_data.get("text", "").strip()
|
||||
state = FSM.get_state(max_user_id)
|
||||
|
||||
if state != BotState.FEATURE_COMMERCIAL:
|
||||
FSM.set_state(max_user_id, BotState.FEATURE_COMMERCIAL)
|
||||
FSM.set_temp(max_user_id, {_STEP_KEY: 0, _COLLECT_KEY: {}})
|
||||
await max_api.send_message(
|
||||
max_user_id,
|
||||
f"📄 **Коммерческое предложение**\n\nШаг 1/{len(_STEPS)}:\n{_STEP_LABELS['service_type']}"
|
||||
)
|
||||
return
|
||||
|
||||
tmp = FSM.get_temp(max_user_id)
|
||||
step = tmp.get(_STEP_KEY, 0)
|
||||
data = tmp.get(_COLLECT_KEY, {})
|
||||
|
||||
if step >= len(_STEPS):
|
||||
await _finish_commercial(bot, user_data, db, max_api, data)
|
||||
return
|
||||
|
||||
step_key = _STEPS[step]
|
||||
if not text and step_key != "comment":
|
||||
await max_api.send_message(max_user_id, f"Пожалуйста, заполните поле:\n{_STEP_LABELS[step_key]}")
|
||||
return
|
||||
|
||||
data[step_key] = text if text else ""
|
||||
step += 1
|
||||
|
||||
if step < len(_STEPS):
|
||||
FSM.set_temp(max_user_id, {_STEP_KEY: step, _COLLECT_KEY: data})
|
||||
next_key = _STEPS[step]
|
||||
await max_api.send_message(
|
||||
max_user_id,
|
||||
f"📄 Шаг {step + 1}/{len(_STEPS)}:\n{_STEP_LABELS[next_key]}"
|
||||
)
|
||||
else:
|
||||
await _finish_commercial(bot, user_data, db, max_api, data)
|
||||
|
||||
|
||||
async def _finish_commercial(bot, user_data, db, max_api, data):
|
||||
max_user_id = user_data["user_id"]
|
||||
user = await get_or_create_user(db, max_user_id)
|
||||
summary = (
|
||||
f"📄 **Коммерческое предложение**\n\n"
|
||||
f"Услуга: {data.get('service_type', '—')}\n"
|
||||
f"Объём: {data.get('scope', '—')}\n"
|
||||
f"Адрес: {data.get('address', '—')}\n"
|
||||
f"Комментарий: {data.get('comment', '—')}\n\n"
|
||||
f"Спасибо, {user.first_name}! Мы подготовим КП и свяжемся с вами."
|
||||
)
|
||||
await max_api.send_message(max_user_id, summary, format="markdown")
|
||||
|
||||
conv_text = (
|
||||
f"Коммерческое предложение: услуга={data.get('service_type', '')}, "
|
||||
f"объём={data.get('scope', '')}, адрес={data.get('address', '')}, "
|
||||
f"комментарий={data.get('comment', '')}"
|
||||
)
|
||||
conv = await _create_commercial_conv(db, user.id, conv_text)
|
||||
from app.integrations import _1c_unf as ic_integration
|
||||
await ic_integration.send_to_1c(db, conv)
|
||||
|
||||
FSM.clear_temp(max_user_id)
|
||||
FSM.transition(max_user_id, "done")
|
||||
buttons = build_main_menu_buttons()
|
||||
menu_text = await get_template_rendered(db, "main_menu")
|
||||
await max_api.send_message_with_keyboard(max_user_id, menu_text, buttons)
|
||||
|
||||
|
||||
async def _create_commercial_conv(db, user_id, details):
|
||||
from app.conversation import create_conversation, add_message
|
||||
from sqlalchemy import select
|
||||
from app.models.models import BotCategory
|
||||
result = await db.execute(
|
||||
select(BotCategory).where(BotCategory.name == "Консультация").limit(1)
|
||||
)
|
||||
cat = result.scalar_one_or_none()
|
||||
conv = await create_conversation(db, user_id)
|
||||
conv.inquiry_text = details
|
||||
conv.inquiry_type = cat.id if cat else None
|
||||
await db.flush()
|
||||
await add_message(db, conv.id, "in", details)
|
||||
return conv
|
||||
@@ -0,0 +1,39 @@
|
||||
from app.settings_cache import SettingsCache
|
||||
from app.conversation import get_or_create_user, get_template_rendered
|
||||
from app.fsm import BotState, FSM
|
||||
from app.keyboards.inline import build_main_menu_buttons
|
||||
|
||||
|
||||
async def handle_my_objects(bot, user_data: dict, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
text = user_data.get("text", "").strip()
|
||||
|
||||
if FSM.get_state(max_user_id) != BotState.FEATURE_MY_OBJECTS:
|
||||
FSM.set_state(max_user_id, BotState.FEATURE_MY_OBJECTS)
|
||||
user = await get_or_create_user(db, max_user_id)
|
||||
await max_api.send_message(max_user_id,
|
||||
"🏢 **Мои объекты**\n\n"
|
||||
"Укажите название вашей компании для поиска объектов."
|
||||
)
|
||||
return
|
||||
|
||||
if not text:
|
||||
await max_api.send_message(max_user_id, "Укажите название компании.")
|
||||
return
|
||||
|
||||
from app.integrations.portal_db import get_objects_for_customer
|
||||
objects = await get_objects_for_customer(db, text)
|
||||
|
||||
if not objects:
|
||||
await max_api.send_message(max_user_id, "Объекты не найдены. Проверьте название компании.")
|
||||
else:
|
||||
lines = [f"🏢 **{obj.name}** — {obj.status}" for obj in objects[:10]]
|
||||
msg = "Ваши объекты:\n\n" + "\n".join(lines)
|
||||
if len(objects) > 10:
|
||||
msg += f"\n\n...и ещё {len(objects) - 10} объектов"
|
||||
await max_api.send_message(max_user_id, msg, format="markdown")
|
||||
|
||||
FSM.transition(max_user_id, "done")
|
||||
buttons = build_main_menu_buttons()
|
||||
menu_text = await get_template_rendered(db, "main_menu")
|
||||
await max_api.send_message_with_keyboard(max_user_id, menu_text, buttons)
|
||||
@@ -0,0 +1,68 @@
|
||||
from app.settings_cache import SettingsCache
|
||||
from app.conversation import get_or_create_user, get_template_rendered
|
||||
from app.fsm import BotState, FSM
|
||||
from app.keyboards.inline import build_main_menu_buttons
|
||||
|
||||
|
||||
async def handle_notifications(bot, user_data: dict, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
payload = user_data.get("payload", "")
|
||||
|
||||
if FSM.get_state(max_user_id) != BotState.FEATURE_NOTIFICATIONS:
|
||||
FSM.set_state(max_user_id, BotState.FEATURE_NOTIFICATIONS)
|
||||
user = await get_or_create_user(db, max_user_id)
|
||||
from sqlalchemy import select
|
||||
from app.models.models import BotNotificationSubscription
|
||||
result = await db.execute(
|
||||
select(BotNotificationSubscription).where(
|
||||
BotNotificationSubscription.user_id == user.id
|
||||
).limit(1)
|
||||
)
|
||||
sub = result.scalar_one_or_none()
|
||||
status_change = "✅" if sub and sub.notify_on_status_change else "⬜"
|
||||
reply = "✅" if sub and sub.notify_on_reply else "⬜"
|
||||
broadcast = "✅" if sub and sub.notify_on_broadcast else "⬜"
|
||||
text = (
|
||||
"🔔 **Уведомления**\n\n"
|
||||
"Выберите, о чём уведомлять:\n\n"
|
||||
f"{status_change} /status — Смена статуса обращения\n"
|
||||
f"{reply} /reply — Ответ оператора\n"
|
||||
f"{broadcast} /broadcast — Новости и рассылки\n\n"
|
||||
"Нажмите на пункт, чтобы переключить."
|
||||
)
|
||||
await max_api.send_message(max_user_id, text, format="markdown")
|
||||
return
|
||||
|
||||
user = await get_or_create_user(db, max_user_id)
|
||||
from sqlalchemy import select
|
||||
from app.models.models import BotNotificationSubscription
|
||||
result = await db.execute(
|
||||
select(BotNotificationSubscription).where(
|
||||
BotNotificationSubscription.user_id == user.id
|
||||
).limit(1)
|
||||
)
|
||||
sub = result.scalar_one_or_none()
|
||||
if not sub:
|
||||
sub = BotNotificationSubscription(user_id=user.id)
|
||||
db.add(sub)
|
||||
await db.flush()
|
||||
|
||||
toggle_map = {
|
||||
"/status": "notify_on_status_change",
|
||||
"/reply": "notify_on_reply",
|
||||
"/broadcast": "notify_on_broadcast",
|
||||
}
|
||||
key = payload if payload.startswith("/") else (payload if payload in toggle_map else user_data.get("text", "").strip())
|
||||
if key in toggle_map:
|
||||
col = toggle_map[key]
|
||||
current = getattr(sub, col)
|
||||
setattr(sub, col, not current)
|
||||
await db.flush()
|
||||
await max_api.send_message(max_user_id, f"Настройка «{key}» изменена.")
|
||||
else:
|
||||
await max_api.send_message(max_user_id, "Нажмите /status, /reply или /broadcast для переключения.")
|
||||
|
||||
FSM.transition(max_user_id, "done")
|
||||
buttons = build_main_menu_buttons()
|
||||
menu_text = await get_template_rendered(db, "main_menu")
|
||||
await max_api.send_message_with_keyboard(max_user_id, menu_text, buttons)
|
||||
@@ -0,0 +1,69 @@
|
||||
from app.settings_cache import SettingsCache
|
||||
from app.conversation import get_or_create_user, get_open_conversation, create_conversation, add_message, get_template_rendered
|
||||
from app.fsm import BotState, FSM
|
||||
from app.keyboards.inline import build_main_menu_buttons
|
||||
|
||||
|
||||
async def handle_photo_report(bot, user_data: dict, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
text = user_data.get("text", "").strip()
|
||||
|
||||
if FSM.get_state(max_user_id) != BotState.FEATURE_PHOTO_REPORT:
|
||||
FSM.set_state(max_user_id, BotState.FEATURE_PHOTO_REPORT)
|
||||
await max_api.send_message(max_user_id,
|
||||
"📸 **Фото-отчёт**\n\n"
|
||||
"Отправьте фото проблемы и опишите, что произошло.\n"
|
||||
"Вы можете прикрепить документ (PDF, DOCX, XLSX, TXT)."
|
||||
)
|
||||
return
|
||||
|
||||
user = await get_or_create_user(db, max_user_id)
|
||||
conv = await get_open_conversation(db, user.id)
|
||||
if not conv:
|
||||
conv = await create_conversation(db, user.id)
|
||||
|
||||
has_attachment = user_data.get("has_attachment", False)
|
||||
attachment_type = user_data.get("attachment_type", "")
|
||||
attachment_path = user_data.get("attachment_path", "")
|
||||
|
||||
conv.inquiry_text = text or "Фото-отчёт"
|
||||
conv.has_attachment = has_attachment
|
||||
conv.attachment_type = attachment_type
|
||||
conv.attachment_path = attachment_path
|
||||
conv.priority = "high"
|
||||
await db.flush()
|
||||
|
||||
await add_message(db, conv.id, "in", text, has_attachment, attachment_type, attachment_path)
|
||||
|
||||
ooh = not SettingsCache.is_work_hours()
|
||||
if ooh:
|
||||
start = SettingsCache.get_int("work_hours_start", 9)
|
||||
end = SettingsCache.get_int("work_hours_end", 18)
|
||||
ooh_text = await get_template_rendered(db, "out_of_hours",
|
||||
work_hours_start=start, work_hours_end=end)
|
||||
await max_api.send_message(max_user_id, ooh_text)
|
||||
|
||||
text = await get_template_rendered(db, "confirmation_ooh" if ooh else "confirmation",
|
||||
first_name=user.first_name, conv_id=conv.id)
|
||||
await max_api.send_message(max_user_id, text, format="markdown")
|
||||
|
||||
from app.integrations import _1c_unf as ic_integration
|
||||
await ic_integration.send_to_1c(db, conv)
|
||||
|
||||
from app.models.models import Incident
|
||||
incident = Incident(
|
||||
object_id=0,
|
||||
reported_by=None,
|
||||
title=f"Фото-отчёт от {user.first_name or 'клиента'}",
|
||||
description=text or "Фото-отчёт через бота",
|
||||
severity="P3",
|
||||
status="open",
|
||||
)
|
||||
db.add(incident)
|
||||
await db.flush()
|
||||
conv.attachment_path = f"incident_{incident.id}"
|
||||
|
||||
FSM.transition(max_user_id, "done")
|
||||
buttons = build_main_menu_buttons()
|
||||
menu_text = await get_template_rendered(db, "main_menu")
|
||||
await max_api.send_message_with_keyboard(max_user_id, menu_text, buttons)
|
||||
@@ -0,0 +1,35 @@
|
||||
from app.settings_cache import SettingsCache
|
||||
from app.conversation import get_or_create_user, get_open_conversation, get_template_rendered
|
||||
from app.keyboards.inline import build_quality_buttons, build_main_menu_buttons
|
||||
from app.fsm import BotState, FSM
|
||||
|
||||
|
||||
async def handle_quality_rate(bot, user_data: dict, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
payload = user_data.get("payload", "")
|
||||
|
||||
if FSM.get_state(max_user_id) != BotState.FEATURE_QUALITY_RATE:
|
||||
FSM.set_state(max_user_id, BotState.FEATURE_QUALITY_RATE)
|
||||
conv = await get_open_conversation(db, (await get_or_create_user(db, max_user_id)).id)
|
||||
conv_id = conv.id if conv else "—"
|
||||
text = await get_template_rendered(db, "quality_request", conv_id=conv_id)
|
||||
buttons = build_quality_buttons()
|
||||
await max_api.send_message_with_keyboard(max_user_id, text, buttons)
|
||||
return
|
||||
|
||||
if payload.startswith("rate_"):
|
||||
try:
|
||||
rating = int(payload.replace("rate_", ""))
|
||||
except ValueError:
|
||||
return
|
||||
await max_api.send_message(max_user_id, f"Спасибо за оценку: {rating}/5! Ваш отзыв важен для нас.")
|
||||
FSM.transition(max_user_id, "done")
|
||||
buttons = build_main_menu_buttons()
|
||||
menu_text = await get_template_rendered(db, "main_menu")
|
||||
await max_api.send_message_with_keyboard(max_user_id, menu_text, buttons)
|
||||
return
|
||||
|
||||
FSM.transition(max_user_id, "done")
|
||||
buttons = build_main_menu_buttons()
|
||||
menu_text = await get_template_rendered(db, "main_menu")
|
||||
await max_api.send_message_with_keyboard(max_user_id, menu_text, buttons)
|
||||
@@ -0,0 +1,48 @@
|
||||
from app.settings_cache import SettingsCache
|
||||
from app.conversation import get_or_create_user, get_template_rendered
|
||||
from app.fsm import BotState, FSM
|
||||
from app.keyboards.inline import build_main_menu_buttons
|
||||
|
||||
|
||||
async def handle_reminders(bot, user_data: dict, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
text = user_data.get("text", "").strip()
|
||||
|
||||
if FSM.get_state(max_user_id) != BotState.FEATURE_REMINDERS:
|
||||
FSM.set_state(max_user_id, BotState.FEATURE_REMINDERS)
|
||||
user = await get_or_create_user(db, max_user_id)
|
||||
phone = user.phone or ""
|
||||
|
||||
from sqlalchemy import select, func
|
||||
from app.models.models import BotConversation, BotTicket, Customer
|
||||
|
||||
open_convs = await db.execute(
|
||||
select(func.count(BotConversation.id))
|
||||
.where(BotConversation.status.in_(["open", "handoff"]))
|
||||
)
|
||||
total_open = open_convs.scalar() or 0
|
||||
|
||||
open_tickets = await db.execute(
|
||||
select(func.count(BotTicket.id))
|
||||
.where(BotTicket.status.in_(["open", "in_progress"]))
|
||||
)
|
||||
total_tickets = open_tickets.scalar() or 0
|
||||
|
||||
my_convs = await db.execute(
|
||||
select(func.count(BotConversation.id))
|
||||
.where(BotConversation.user_id == user.id, BotConversation.status.in_(["open", "handoff"]))
|
||||
)
|
||||
my_open = my_convs.scalar() or 0
|
||||
|
||||
text = (
|
||||
"🔔 **Напоминания**\n\n"
|
||||
f"У вас открыто обращений: {my_open}\n"
|
||||
f"Всего в системе: {total_open} обращений, {total_tickets} заявок\n\n"
|
||||
"Напоминания о плановом ТО настраиваются в портале."
|
||||
)
|
||||
await max_api.send_message(max_user_id, text, format="markdown")
|
||||
FSM.transition(max_user_id, "done")
|
||||
buttons = build_main_menu_buttons()
|
||||
menu_text = await get_template_rendered(db, "main_menu")
|
||||
await max_api.send_message_with_keyboard(max_user_id, menu_text, buttons)
|
||||
return
|
||||
@@ -0,0 +1,100 @@
|
||||
from sqlalchemy import select
|
||||
from app.settings_cache import SettingsCache
|
||||
from app.conversation import get_or_create_user, get_template_rendered
|
||||
from app.fsm import BotState, FSM
|
||||
from app.keyboards.inline import build_main_menu_buttons
|
||||
|
||||
_ANSWERS_KEY = "risk_answers"
|
||||
_STEP_KEY = "risk_step"
|
||||
|
||||
|
||||
async def _load_questions(db):
|
||||
from app.models.models import BotRiskQuestion
|
||||
result = await db.execute(
|
||||
select(BotRiskQuestion)
|
||||
.where(BotRiskQuestion.is_active == True)
|
||||
.order_by(BotRiskQuestion.sort_order)
|
||||
)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
async def handle_risk_score(bot, user_data: dict, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
text = user_data.get("text", "").strip()
|
||||
state = FSM.get_state(max_user_id)
|
||||
|
||||
questions = await _load_questions(db)
|
||||
if not questions:
|
||||
await max_api.send_message(max_user_id, "Анкета риск-инжиниринга временно недоступна.")
|
||||
FSM.transition(max_user_id, "cancel")
|
||||
buttons = build_main_menu_buttons()
|
||||
menu_text = await get_template_rendered(db, "main_menu")
|
||||
await max_api.send_message_with_keyboard(max_user_id, menu_text, buttons)
|
||||
return
|
||||
|
||||
if state != BotState.FEATURE_RISK_SCORE:
|
||||
FSM.set_state(max_user_id, BotState.FEATURE_RISK_SCORE)
|
||||
FSM.set_temp(max_user_id, {_STEP_KEY: 0, _ANSWERS_KEY: {}})
|
||||
q = questions[0]
|
||||
await max_api.send_message(
|
||||
max_user_id,
|
||||
f"📊 **Risk Score — анкета**\n\nВопрос 1/{len(questions)}:\n{q.question}\n\nОтветьте «да» или «нет»."
|
||||
)
|
||||
return
|
||||
|
||||
tmp = FSM.get_temp(max_user_id)
|
||||
step = tmp.get(_STEP_KEY, 0)
|
||||
answers = tmp.get(_ANSWERS_KEY, {})
|
||||
|
||||
if step >= len(questions):
|
||||
await _finish_risk_score(max_user_id, answers, questions, db, max_api)
|
||||
return
|
||||
|
||||
if text.lower() in ("да", "yes", "+", "1"):
|
||||
answers[questions[step].question_key] = True
|
||||
elif text.lower() in ("нет", "no", "-", "0"):
|
||||
answers[questions[step].question_key] = False
|
||||
else:
|
||||
await max_api.send_message(max_user_id, "Пожалуйста, ответьте «да» или «нет».")
|
||||
return
|
||||
|
||||
step += 1
|
||||
if step < len(questions):
|
||||
q = questions[step]
|
||||
FSM.set_temp(max_user_id, {_STEP_KEY: step, _ANSWERS_KEY: answers})
|
||||
await max_api.send_message(
|
||||
max_user_id,
|
||||
f"📊 Вопрос {step + 1}/{len(questions)}:\n{q.question}\n\n«да» или «нет»?"
|
||||
)
|
||||
else:
|
||||
await _finish_risk_score(max_user_id, answers, questions, db, max_api)
|
||||
|
||||
|
||||
async def _finish_risk_score(max_user_id, answers, questions, db, max_api):
|
||||
score = 0
|
||||
for q in questions:
|
||||
if answers.get(q.question_key):
|
||||
score += q.weight
|
||||
score = min(score, 100)
|
||||
|
||||
if score < 20:
|
||||
label = "Низкий"
|
||||
rec = "Поддерживайте текущий уровень обслуживания"
|
||||
elif score < 50:
|
||||
label = "Средний"
|
||||
rec = "Рекомендуем провести аудит систем"
|
||||
elif score < 75:
|
||||
label = "Высокий"
|
||||
rec = "Необходимо срочное обслуживание"
|
||||
else:
|
||||
label = "Критический"
|
||||
rec = "Требуется немедленное вмешательство"
|
||||
|
||||
tmpl = await get_template_rendered(db, "risk_result", score=score, label=label, recommendation=rec)
|
||||
await max_api.send_message(max_user_id, tmpl, format="markdown")
|
||||
|
||||
FSM.clear_temp(max_user_id)
|
||||
FSM.transition(max_user_id, "done")
|
||||
buttons = build_main_menu_buttons()
|
||||
menu_text = await get_template_rendered(db, "main_menu")
|
||||
await max_api.send_message_with_keyboard(max_user_id, menu_text, buttons)
|
||||
@@ -0,0 +1,54 @@
|
||||
from app.settings_cache import SettingsCache
|
||||
from app.conversation import get_or_create_user, get_template_rendered
|
||||
from app.fsm import BotState, FSM
|
||||
from app.keyboards.inline import build_main_menu_buttons
|
||||
|
||||
|
||||
async def handle_sla_status(bot, user_data: dict, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
text = user_data.get("text", "").strip()
|
||||
|
||||
if FSM.get_state(max_user_id) != BotState.FEATURE_SLA_STATUS:
|
||||
FSM.set_state(max_user_id, BotState.FEATURE_SLA_STATUS)
|
||||
await max_api.send_message(max_user_id,
|
||||
"🔍 **Проверка SLA статуса**\n\n"
|
||||
"Укажите номер или название объекта."
|
||||
)
|
||||
return
|
||||
|
||||
if not text:
|
||||
await max_api.send_message(max_user_id, "Укажите номер или название объекта.")
|
||||
return
|
||||
|
||||
from app.integrations.portal_db import get_object_by_name_or_id, get_sla_for_object, get_last_incident_for_object, get_active_tasks_for_object
|
||||
obj = await get_object_by_name_or_id(db, text)
|
||||
if not obj:
|
||||
await max_api.send_message(max_user_id, "Объект не найден.")
|
||||
FSM.transition(max_user_id, "done")
|
||||
buttons = build_main_menu_buttons()
|
||||
menu_text = await get_template_rendered(db, "main_menu")
|
||||
await max_api.send_message_with_keyboard(max_user_id, menu_text, buttons)
|
||||
return
|
||||
|
||||
sla = await get_sla_for_object(db, obj.id)
|
||||
if not sla:
|
||||
text = await get_template_rendered(db, "sla_not_found")
|
||||
await max_api.send_message(max_user_id, text)
|
||||
else:
|
||||
level_map = {"start": "Базовый", "business": "Оптимальный", "enterprise": "Максимальный"}
|
||||
last_incident = await get_last_incident_for_object(db, obj.id)
|
||||
open_tasks = await get_active_tasks_for_object(db, obj.id)
|
||||
last_visit = last_incident.created_at.strftime("%d.%m.%Y") if last_incident else "—"
|
||||
tasks_count = len(open_tasks)
|
||||
text = await get_template_rendered(db, "sla_found",
|
||||
object_name=obj.name,
|
||||
service_level=level_map.get(sla.service_level, sla.service_level),
|
||||
status=sla.status,
|
||||
price=sla.sla_price_monthly or 0)
|
||||
text += f"\n📅 Последний визит: {last_visit}\n📋 Открытых задач: {tasks_count}"
|
||||
await max_api.send_message(max_user_id, text, format="markdown")
|
||||
|
||||
FSM.transition(max_user_id, "done")
|
||||
buttons = build_main_menu_buttons()
|
||||
menu_text = await get_template_rendered(db, "main_menu")
|
||||
await max_api.send_message_with_keyboard(max_user_id, menu_text, buttons)
|
||||
@@ -0,0 +1,155 @@
|
||||
from app.settings_cache import SettingsCache
|
||||
from app.conversation import get_or_create_user, get_template_rendered
|
||||
from app.fsm import BotState, FSM
|
||||
from app.keyboards.inline import build_main_menu_buttons
|
||||
|
||||
|
||||
async def handle_sla_ticket(bot, user_data: dict, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
text = user_data.get("text", "").strip()
|
||||
|
||||
if FSM.get_state(max_user_id) != BotState.SLA_TICKET_CREATE:
|
||||
FSM.set_state(max_user_id, BotState.SLA_TICKET_CREATE)
|
||||
await max_api.send_message(max_user_id, "🔧 **Заявка по SLA**\n\nПроверяю ваш SLA контракт...")
|
||||
|
||||
user = await get_or_create_user(db, max_user_id)
|
||||
phone = user.phone or ""
|
||||
if not phone:
|
||||
await max_api.send_message(max_user_id, "Не могу найти ваш номер телефона. Укажите его, пожалуйста.")
|
||||
return
|
||||
|
||||
from sqlalchemy import select
|
||||
from app.models.models import Customer, Object, SLAContract
|
||||
result = await db.execute(
|
||||
select(Customer).where(Customer.contact_phone.ilike(f"%{phone[-10:]}%")).limit(1)
|
||||
)
|
||||
customer = result.scalar_one_or_none()
|
||||
if not customer:
|
||||
await _send_no_sla(bot, user_data, db, max_api)
|
||||
return
|
||||
|
||||
result = await db.execute(
|
||||
select(Object).where(Object.customer_id == customer.id, Object.status == "active").limit(1)
|
||||
)
|
||||
obj = result.scalar_one_or_none()
|
||||
if not obj:
|
||||
await _send_no_sla(bot, user_data, db, max_api)
|
||||
return
|
||||
|
||||
result = await db.execute(
|
||||
select(SLAContract).where(
|
||||
SLAContract.object_id == obj.id,
|
||||
SLAContract.status == "active"
|
||||
).order_by(SLAContract.created_at.desc()).limit(1)
|
||||
)
|
||||
sla = result.scalar_one_or_none()
|
||||
if not sla:
|
||||
await _send_no_sla(bot, user_data, db, max_api)
|
||||
return
|
||||
|
||||
level_map = {"start": "Базовый", "business": "Оптимальный", "enterprise": "Максимальный"}
|
||||
text = await get_template_rendered(db, "sla_ticket_found",
|
||||
object_name=obj.name,
|
||||
service_level=level_map.get(sla.service_level, sla.service_level))
|
||||
await max_api.send_message(max_user_id, text, format="markdown")
|
||||
return
|
||||
|
||||
if not text:
|
||||
await max_api.send_message(max_user_id, "Опишите неисправность. Вы можете прикрепить фото или документ.")
|
||||
return
|
||||
|
||||
user = await get_or_create_user(db, max_user_id)
|
||||
|
||||
from sqlalchemy import select, func
|
||||
from app.models.models import Customer, Object, SLAContract, BotTicket
|
||||
result = await db.execute(
|
||||
select(Customer).where(Customer.contact_phone.ilike(f"%{user.phone[-10:]}%")).limit(1)
|
||||
)
|
||||
customer = result.scalar_one_or_none()
|
||||
obj = None
|
||||
sla = None
|
||||
if customer:
|
||||
result = await db.execute(
|
||||
select(Object).where(Object.customer_id == customer.id, Object.status == "active").limit(1)
|
||||
)
|
||||
obj = result.scalar_one_or_none()
|
||||
if obj:
|
||||
result = await db.execute(
|
||||
select(SLAContract).where(
|
||||
SLAContract.object_id == obj.id, SLAContract.status == "active"
|
||||
).order_by(SLAContract.created_at.desc()).limit(1)
|
||||
)
|
||||
sla = result.scalar_one_or_none()
|
||||
|
||||
priority_map = {"start": "low", "business": "normal", "enterprise": "high"}
|
||||
response_map = {"start": "4 часа", "business": "2 часа", "enterprise": "30 минут"}
|
||||
sla_level = sla.service_level if sla else "start"
|
||||
priority = priority_map.get(sla_level, "normal")
|
||||
|
||||
result = await db.execute(select(func.count(BotTicket.id)))
|
||||
ticket_num = f"T-{result.scalar() + 1:04d}"
|
||||
|
||||
from app.models.models import BotTicket, BotTicketMessage, BotTicketStatus
|
||||
ticket = BotTicket(
|
||||
ticket_number=ticket_num,
|
||||
user_id=user.id,
|
||||
object_id=obj.id if obj else None,
|
||||
customer_id=customer.id if customer else None,
|
||||
title=text[:100],
|
||||
description=text,
|
||||
priority=priority,
|
||||
status="open",
|
||||
created_by="bot",
|
||||
sla_contract_id=sla.id if sla else None,
|
||||
has_attachment=user_data.get("has_attachment", False),
|
||||
attachment_type=user_data.get("attachment_type", ""),
|
||||
attachment_path=user_data.get("attachment_path", ""),
|
||||
)
|
||||
db.add(ticket)
|
||||
await db.flush()
|
||||
|
||||
db.add(BotTicketMessage(ticket_id=ticket.id, direction="in", text=text, sender="client"))
|
||||
db.add(BotTicketStatus(ticket_id=ticket.id, old_status="", new_status="open", changed_by="bot"))
|
||||
await db.flush()
|
||||
|
||||
has_attachment = user_data.get("has_attachment", False)
|
||||
attachment_type = user_data.get("attachment_type", "")
|
||||
attachment_path = user_data.get("attachment_path", "")
|
||||
if has_attachment:
|
||||
ticket.has_attachment = True
|
||||
ticket.attachment_type = attachment_type
|
||||
ticket.attachment_path = attachment_path
|
||||
await db.flush()
|
||||
|
||||
text = await get_template_rendered(db, "sla_ticket_created",
|
||||
ticket_number=ticket_num,
|
||||
priority=priority,
|
||||
response_time=response_map.get(sla_level, "2 часа"))
|
||||
await max_api.send_message(max_user_id, text, format="markdown")
|
||||
|
||||
from app.max_notifications import notify_ticket_created as nt
|
||||
await nt(db, ticket, max_api)
|
||||
|
||||
FSM.transition(max_user_id, "done")
|
||||
buttons = build_main_menu_buttons()
|
||||
menu_text = await get_template_rendered(db, "main_menu")
|
||||
await max_api.send_message_with_keyboard(max_user_id, menu_text, buttons)
|
||||
|
||||
|
||||
async def _send_no_sla(bot, user_data, db, max_api):
|
||||
max_user_id = user_data["user_id"]
|
||||
await SettingsCache.refresh(db)
|
||||
phone_1 = SettingsCache.get("phone_1")
|
||||
phone_2 = SettingsCache.get("phone_2")
|
||||
email = SettingsCache.get("support_email")
|
||||
text = await get_template_rendered(db, "sla_ticket_not_found",
|
||||
phone_1=phone_1, phone_2=phone_2,
|
||||
support_email=email)
|
||||
from app.keyboards.inline import build_phone_email_buttons
|
||||
subject = SettingsCache.get("email_subject")
|
||||
buttons = build_phone_email_buttons(phone_1, phone_2, email, subject)
|
||||
await max_api.send_message_with_keyboard(max_user_id, text, buttons, format="markdown")
|
||||
FSM.transition(max_user_id, "done")
|
||||
buttons = build_main_menu_buttons()
|
||||
menu_text = await get_template_rendered(db, "main_menu")
|
||||
await max_api.send_message_with_keyboard(max_user_id, menu_text, buttons)
|
||||
Reference in New Issue
Block a user