diff --git a/bot.py b/bot.py index aee5f29..4618ce9 100644 --- a/bot.py +++ b/bot.py @@ -600,6 +600,35 @@ class VKBotApplication: # Маршрутизация по состояниям (FSM) if state == "FREE_CHAT": self._handle_free_chat(user_id, text_clean, text_lower) + # --- Воронка Кибершколы БАЗА --- + elif state == "BAZA_FORWHO": + self._handle_baza_forwho(user_id, text_clean, text_lower) + elif state == "BAZA_AGE": + self._handle_baza_age(user_id, text_clean, text_lower) + elif state == "BAZA_YOUNG": + self._handle_baza_young(user_id, text_clean, text_lower) + elif state == "BAZA_GAME": + self._handle_baza_game(user_id, text_clean, text_lower) + elif state == "BAZA_GAME_OTHER": + self._handle_baza_game_other(user_id, text_clean, text_lower) + elif state == "BAZA_LEVEL": + self._handle_baza_level(user_id, text_clean, text_lower) + elif state == "BAZA_FORMAT": + self._handle_baza_format(user_id, text_clean, text_lower) + elif state == "BAZA_GROUP": + self._handle_baza_group(user_id, text_clean, text_lower) + elif state == "BAZA_INDIVIDUAL": + self._handle_baza_individual(user_id, text_clean, text_lower) + elif state == "BAZA_HELP": + self._handle_baza_help(user_id, text_clean, text_lower) + elif state == "BAZA_PAY": + self._handle_baza_pay(user_id, text_clean, text_lower) + elif state == "BAZA_INT_INFO": + self._handle_baza_int_info(user_id, text_clean, text_lower) + elif state == "BAZA_INT_AGE": + self._handle_baza_int_age(user_id, text_clean, text_lower) + elif state == "BAZA_INT_LEVEL": + self._handle_baza_int_level(user_id, text_clean, text_lower) elif state == "FORM_RETURNING": self._handle_form_returning(user_id, text_clean, text_lower, user_data) elif state == "FORM_RET_NAME": @@ -670,104 +699,72 @@ class VKBotApplication: # ------------------------------------------ def _handle_free_chat(self, user_id: int, text: str, text_lower: str): - """Логика Свободного Чата (Главное меню, FAQ, переключение на форму).""" - - # 1. Запуск формы опроса - if text_lower in ["заполнить заявку 📝", "заявка", "начать опрос", "опрос"]: - user_data = db.get_user_data(user_id) - # Если клиент уже оставлял данные — предлагаем подтвердить их, а не вводить заново - if user_data.get("name") and user_data.get("phone"): - db.update_user_state(user_id, "FORM_RETURNING") - self._send_returning_prompt(user_id, user_data) - else: - db.update_user_state(user_id, "FORM_NAME") - self.send_message( - user_id, - "📝 Начинаем заполнение заявки!\n\n" - f"{self._progress_bar(1)}\nКак к вам обращаться? Пожалуйста, введите ваше имя.", - keyboard=get_cancel_keyboard() - ) + """Главное меню Кибершколы БАЗА: воронка подбора, интенсив, цены, вопрос.""" + + # 1. Подобрать тренировку — запуск воронки + if text_lower in ["подобрать тренировку 🎯", "подобрать тренировку", "подобрать заново 🎯", + "подобрать", "подбор", "записаться", "тренировки", "занятия"]: + self._baza_start_funnel(user_id) return - # 1.5 Площадки ИКП — кнопки-ссылки + # 2. Летний интенсив CS2 (+ кодовые слова «лето», «интенсив» и т.п.) + if text_lower in ["летний интенсив cs2 🔥", "летний интенсив cs2", "летний интенсив", "интенсив"] \ + or any(t in text_lower for t in config.SUMMER_TRIGGERS): + self._baza_start_intensive(user_id) + return + + # 3. Посмотреть цены + if text_lower in ["посмотреть цены 💰", "посмотреть цены", "цены", "цена", "стоимость", "прайс"]: + self._baza_show_prices(user_id) + return + + # 3.1 Кнопки под ценами + if text_lower in ["записаться на пробное", "пробное", "пробное занятие"]: + self._baza_send_payment(user_id, "trial", "Пробное занятие") + return + if text_lower in ["выбрать абонемент", "абонемент", "абонементы"]: + # Подбираем формат через воронку, затем предложим точный тариф + self._baza_start_funnel(user_id) + return + + # 4. Задать вопрос / оператор / менеджер — передаём менеджеру + if text_lower in ["задать вопрос 💬", "задать вопрос", "вопрос", "задать вопрос менеджеру 💬", + "оператор", "менеджер", "человек", "позвать оператора", "связь с менеджером"]: + self._baza_to_manager(user_id, reason="Прямой вопрос из меню") + return + + # 5. Площадки — кнопки-ссылки if text_lower in ["наши соцсети 🔗", "наши соцсети", "соцсети", "площадки", "ссылки"]: self.send_message( user_id, - "🔗 Мы на площадках ИКП — заходите, подписывайтесь и смотрите трансляции 👇", + "🔗 Мы на площадках Кибершколы БАЗА — заходите и подписывайтесь 👇", keyboard=get_socials_keyboard() ) return - # 2. Переход к списку частых вопросов (FAQ) - if text_lower in ["частые вопросы ❓", "faq", "вопросы", "помощь"]: + # 6. Возврат в главное меню + if text_lower in ["назад в меню ↩️", "меню", "назад", "в главное меню ↩️", "главное меню"]: self.send_message( user_id, - "❓ Выберите тему, которая вас интересует, на клавиатуре ниже:", - keyboard=get_faq_keyboard() - ) - # Дополнительно — быстрые ответы из админки инлайн-кнопками - custom = db.get_custom_faq() - if custom: - kb = VkKeyboard(inline=True) - for n, (_id, kw, _ans) in enumerate(custom[:6]): - if n and n % 2 == 0: - kb.add_line() - kb.add_button(kw[:40], color=VkKeyboardColor.SECONDARY) - self.send_message( - user_id, - "💬 Популярные вопросы — нажмите, чтобы получить ответ:", - keyboard=kb.get_keyboard() - ) - return - - # 3. Возврат в главное меню - if text_lower in ["назад в меню ↩️", "меню", "назад", "в главное меню ↩️"]: - self.send_message( - user_id, - "🏠 Вы вернулись в главное меню. Выберите интересующее действие:", + "🏠 Главное меню Кибершколы БАЗА. Чем помочь?", keyboard=get_main_keyboard(is_admin=self._is_admin(user_id)) ) return - # 3.5 Раздел "Карьера в ИКП" (набор игроков и сотрудников) - if text_lower in ["карьера в икп 💼", "карьера", "вакансии", "вакансия", "работа", "набор", "хочу в команду"]: - db.clear_vac_data(user_id) - db.update_user_state(user_id, "VAC_CHOOSE") - self._send_career_choice(user_id) + # 7. Приветствие (+ основные триггеры запуска) + if text_lower in ["начать", "привет", "здравствуйте", "ку", "hi", "hello", "старт"] \ + or any(t in text_lower for t in config.MAIN_TRIGGERS): + self._baza_send_greeting(user_id) return - # 4. Вызов оператора - if text_lower in ["позвать оператора 🧑‍💼", "позвать оператора", "оператор", "менеджер", "человек", "позови оператора", "связь с оператором"]: - self._handoff_to_operator(user_id) - return - - # 5. Обработка конкретных ответов FAQ - for key, answer in config.FAQ_ANSWERS.items(): - if key in text_lower: - self.send_message(user_id, answer, keyboard=get_faq_keyboard()) - return - - # 6. Приветственные сообщения - if text_lower in ["начать", "привет", "здравствуйте", "ку", "hi", "hello"]: - self.send_message( - user_id, - "👋 Здравствуйте! Это чат-бот киберспортивной организации ИКП — Игра. Киберспорт. Профессионалы.\n\n" - "Я помогу быстро оформить заявку на тренировки, рассказать об услугах или связать вас с командой. " - "Также у нас открыт набор игроков и сотрудников — загляните в раздел «Карьера в ИКП 💼».\n\n" - "Выберите действие на кнопках ниже 👇", - keyboard=get_main_keyboard(is_admin=self._is_admin(user_id)), - attachment=self.get_logo_attachment(user_id) - ) - return - - # 6.5 Быстрые ответы по ключевым словам (редактируются админом) + # 8. Быстрые ответы по ключевым словам (редактируются админом) quick = db.match_custom_faq(text_lower) if quick: self.send_message(user_id, quick, keyboard=get_main_keyboard(is_admin=self._is_admin(user_id))) return - - # 6.7 Автору поесть (запрет на удаление) - if text_lower in ["/favi","/Favi", "/FAVI" ]: + + # 9. Пасхалка автора (запрет на удаление) + if text_lower in ["/favi", "/Favi", "/FAVI"]: self.send_message( user_id, "Пасихалка для знающих\n\n" @@ -777,14 +774,421 @@ class VKBotApplication: keyboard=get_main_keyboard(is_admin=self._is_admin(user_id)), ) return - # 7. Fallback (Бот не понял, но пытается помочь, предлагая клавиатуру) + + # 10. Fallback — не поняли запрос self.send_message( user_id, - "🤔 Я пока только учусь и не смог полностью распознать ваш запрос.\n\n" - "Пожалуйста, воспользуйтесь кнопками меню ниже или напишите 'оператор', чтобы я пригласил специалиста.", + "🤔 Не совсем понял запрос. Давайте я помогу подобрать тренировку или отвечу на вопрос — " + "выберите на кнопках ниже 👇", keyboard=get_main_keyboard(is_admin=self._is_admin(user_id)) ) - + + # ========================================================== + # ВОРОНКА КИБЕРШКОЛЫ БАЗА (подбор тренировки, интенсив, оплата) + # ========================================================== + + def _utm(self, url: str, campaign: str = "main") -> str: + """Добавляет UTM-метку к ссылке (main — обычные, summer — интенсив).""" + utm = config.UTM_SUMMER if campaign == "summer" else config.UTM_MAIN + sep = "&" if "?" in url else "?" + return f"{url}{sep}{utm}" + + def _baza_send_greeting(self, user_id: int): + db.add_metka(user_id, "bot_started") + self.send_message( + user_id, + config.GREETING_BAZA + "\n\nВыберите действие на кнопках ниже 👇", + keyboard=get_main_keyboard(is_admin=self._is_admin(user_id)), + attachment=self.get_logo_attachment(user_id) + ) + + def _baza_nav(self, user_id: int, text_lower: str) -> bool: + """Общие кнопки внутри воронки: меню / вопрос менеджеру. True — если обработали.""" + if text_lower in ["в главное меню ↩️", "главное меню", "меню", "в меню", "выход"]: + db.update_user_state(user_id, "FREE_CHAT") + self.send_message( + user_id, "🏠 Главное меню Кибершколы БАЗА. Чем помочь?", + keyboard=get_main_keyboard(is_admin=self._is_admin(user_id)) + ) + return True + if text_lower in ["задать вопрос менеджеру 💬", "задать вопрос менеджеру", "вопрос менеджеру", + "менеджер", "оператор"]: + self._baza_to_manager(user_id, reason="Вопрос из подбора тренировки") + return True + return False + + def _baza_start_funnel(self, user_id: int): + """Старт воронки «Подобрать тренировку».""" + db.clear_profile(user_id) + db.set_profile_field(user_id, "source", "Подбор тренировки") + db.add_metka(user_id, "bot_started") + db.add_metka(user_id, "funnel_started") + db.update_user_state(user_id, "BAZA_FORWHO") + self.send_message( + user_id, + "Отлично! Задам пару коротких вопросов и подберу подходящий формат 🎯\n\n" + "Для кого подбираем тренировки?", + keyboard=get_baza_forwho_keyboard() + ) + + def _handle_baza_forwho(self, user_id: int, text: str, text_lower: str): + if self._baza_nav(user_id, text_lower): + return + if text_lower in ["для себя", "себе", "для меня"]: + db.set_profile_field(user_id, "role", "Для себя (занимается сам)") + db.add_metka(user_id, "role_self") + elif text_lower in ["для подростка", "для ребёнка", "для ребенка", "для сына", "для дочери", "для подростка "]: + db.set_profile_field(user_id, "role", "Для подростка (пишет родитель)") + db.add_metka(user_id, "role_parent") + else: + self.send_message(user_id, "Выберите вариант на кнопках ниже 👇", keyboard=get_baza_forwho_keyboard()) + return + db.update_user_state(user_id, "BAZA_AGE") + self.send_message(user_id, "Сколько лет будущему ученику?", keyboard=get_baza_age_keyboard()) + + def _handle_baza_age(self, user_id: int, text: str, text_lower: str): + if self._baza_nav(user_id, text_lower): + return + if text_lower.startswith("назад"): + db.update_user_state(user_id, "BAZA_FORWHO") + self.send_message(user_id, "Для кого подбираем тренировки?", keyboard=get_baza_forwho_keyboard()) + return + if "младше 12" in text_lower or "меньше 12" in text_lower: + db.set_profile_field(user_id, "age", "Младше 12 лет") + db.add_metka(user_id, "age_under12") + db.update_user_state(user_id, "BAZA_YOUNG") + self.send_message( + user_id, + "Обычно мы занимаемся с ребятами от 12 лет. Если ученику чуть меньше — это не проблема, " + "лучше подобрать формат вместе с менеджером.", + keyboard=get_baza_younger12_keyboard() + ) + return + if "12" in text_lower or "13" in text_lower: + age, metka = "12–13 лет", "age_12_13" + elif "14" in text_lower or "15" in text_lower: + age, metka = "14–15 лет", "age_14_15" + elif "16" in text_lower: + age, metka = "16 лет и старше", "age_16plus" + else: + self.send_message(user_id, "Выберите возраст на кнопках ниже 👇", keyboard=get_baza_age_keyboard()) + return + db.set_profile_field(user_id, "age", age) + db.add_metka(user_id, metka) + db.update_user_state(user_id, "BAZA_GAME") + self.send_message(user_id, "Какая игра интересует?", keyboard=get_baza_game_keyboard()) + + def _handle_baza_young(self, user_id: int, text: str, text_lower: str): + if text_lower.startswith("обсудить"): + self._baza_to_manager(user_id, reason="Ученик младше 12 лет") + return + if text_lower.startswith("назад") or text_lower.startswith("вернуться"): + db.update_user_state(user_id, "BAZA_AGE") + self.send_message(user_id, "Сколько лет будущему ученику?", keyboard=get_baza_age_keyboard()) + return + if self._baza_nav(user_id, text_lower): + return + self.send_message(user_id, "Выберите вариант на кнопках ниже 👇", keyboard=get_baza_younger12_keyboard()) + + def _handle_baza_game(self, user_id: int, text: str, text_lower: str): + if self._baza_nav(user_id, text_lower): + return + if text_lower.startswith("назад"): + db.update_user_state(user_id, "BAZA_AGE") + self.send_message(user_id, "Сколько лет будущему ученику?", keyboard=get_baza_age_keyboard()) + return + if "cs" in text_lower or "кс" in text_lower or "контр" in text_lower: + game, metka = "CS2", "game_cs2" + elif "dota" in text_lower or "дота" in text_lower: + game, metka = "Dota 2", "game_dota" + elif "tank" in text_lower or "танк" in text_lower or "wot" in text_lower: + game, metka = "World of Tanks", "game_wot" + elif "друг" in text_lower: + db.update_user_state(user_id, "BAZA_GAME_OTHER") + self.send_message( + user_id, "Напишите, какая игра интересует — передам менеджеру, он подскажет по возможностям.", + keyboard=get_baza_game_keyboard() + ) + return + else: + self.send_message(user_id, "Выберите игру на кнопках ниже 👇", keyboard=get_baza_game_keyboard()) + return + db.set_profile_field(user_id, "game", game) + db.add_metka(user_id, metka) + db.update_user_state(user_id, "BAZA_LEVEL") + self.send_message(user_id, "Какой сейчас уровень игры у ученика?", keyboard=get_baza_level_keyboard()) + + def _handle_baza_game_other(self, user_id: int, text: str, text_lower: str): + if self._baza_nav(user_id, text_lower): + return + if text_lower.startswith("назад"): + db.update_user_state(user_id, "BAZA_GAME") + self.send_message(user_id, "Какая игра интересует?", keyboard=get_baza_game_keyboard()) + return + db.set_profile_field(user_id, "game", f"Другая игра: {text.strip()}") + db.add_metka(user_id, "game_other") + self._baza_to_manager(user_id, reason="Интересует другая игра") + + def _handle_baza_level(self, user_id: int, text: str, text_lower: str): + if self._baza_nav(user_id, text_lower): + return + if text_lower.startswith("назад"): + db.update_user_state(user_id, "BAZA_GAME") + self.send_message(user_id, "Какая игра интересует?", keyboard=get_baza_game_keyboard()) + return + levels = { + "только начинает": ("Только начинает", "level_novice"), + "играет для себя": ("Играет для себя", "level_casual"), + "есть рейтинг или ранг": ("Есть рейтинг/ранг", "level_ranked"), + "играет в команде или турнирах": ("Команда/турниры", "level_pro"), + } + match = levels.get(text_lower) + if not match: + self.send_message(user_id, "Выберите уровень на кнопках ниже 👇", keyboard=get_baza_level_keyboard()) + return + db.set_profile_field(user_id, "level", match[0]) + db.add_metka(user_id, match[1]) + db.update_user_state(user_id, "BAZA_FORMAT") + self.send_message(user_id, "Какой формат тренировок предпочтительнее?", keyboard=get_baza_format_keyboard()) + + def _handle_baza_format(self, user_id: int, text: str, text_lower: str): + if self._baza_nav(user_id, text_lower): + return + if text_lower.startswith("назад"): + db.update_user_state(user_id, "BAZA_LEVEL") + self.send_message(user_id, "Какой сейчас уровень игры у ученика?", keyboard=get_baza_level_keyboard()) + return + if "групп" in text_lower: + db.set_profile_field(user_id, "format", "Групповые тренировки") + db.add_metka(user_id, "format_group") + db.update_user_state(user_id, "BAZA_GROUP") + self.send_message( + user_id, + "Групповые тренировки — занятия в мини-группе с тренером.\n\n" + "🧑‍🤝‍🧑 Групповые:\n• 4 занятия — 2 790 ₽\n• 8 занятий — 4 990 ₽\n\n" + "Можно начать с пробного занятия. Что выберете?", + keyboard=get_baza_group_keyboard() + ) + elif "индивид" in text_lower: + db.set_profile_field(user_id, "format", "Индивидуальные тренировки") + db.add_metka(user_id, "format_individual") + db.update_user_state(user_id, "BAZA_INDIVIDUAL") + self.send_message( + user_id, + "Индивидуальные тренировки — персональная работа с тренером один на один.\n\n" + "👤 Индивидуальные:\n• 1 занятие — 870 ₽\n• 4 занятия — 3 290 ₽\n• 8 занятий — 5 990 ₽\n\n" + "Можно начать с пробного занятия. Что выберете?", + keyboard=get_baza_individual_keyboard() + ) + elif "не знаю" in text_lower or "совет" in text_lower: + db.set_profile_field(user_id, "format", "Нужен совет по формату") + db.add_metka(user_id, "need_advice") + db.update_user_state(user_id, "BAZA_HELP") + self.send_message( + user_id, + "Без проблем — подскажем 🙌 Можно записаться на пробное занятие и определиться на практике " + "или задать вопрос менеджеру, он поможет выбрать формат.", + keyboard=get_baza_help_keyboard() + ) + else: + self.send_message(user_id, "Выберите формат на кнопках ниже 👇", keyboard=get_baza_format_keyboard()) + + def _handle_baza_group(self, user_id: int, text: str, text_lower: str): + if self._baza_nav(user_id, text_lower): + return + if text_lower.startswith("назад"): + db.update_user_state(user_id, "BAZA_FORMAT") + self.send_message(user_id, "Какой формат тренировок предпочтительнее?", keyboard=get_baza_format_keyboard()) + return + if "пробное" in text_lower: + self._baza_send_payment(user_id, "trial", "Пробное занятие (группа)") + elif "4" in text_lower: + self._baza_send_payment(user_id, "group_4", "Групповой абонемент, 4 занятия") + elif "8" in text_lower: + self._baza_send_payment(user_id, "group_8", "Групповой абонемент, 8 занятий") + else: + self.send_message(user_id, "Выберите вариант на кнопках ниже 👇", keyboard=get_baza_group_keyboard()) + + def _handle_baza_individual(self, user_id: int, text: str, text_lower: str): + if self._baza_nav(user_id, text_lower): + return + if text_lower.startswith("назад"): + db.update_user_state(user_id, "BAZA_FORMAT") + self.send_message(user_id, "Какой формат тренировок предпочтительнее?", keyboard=get_baza_format_keyboard()) + return + if "пробное" in text_lower: + self._baza_send_payment(user_id, "trial", "Пробное занятие (индивид.)") + elif "1" in text_lower: + self._baza_send_payment(user_id, "individual_1", "Индивидуальное, 1 занятие") + elif "4" in text_lower: + self._baza_send_payment(user_id, "individual_4", "Индивидуальные, 4 занятия") + elif "8" in text_lower: + self._baza_send_payment(user_id, "individual_8", "Индивидуальные, 8 занятий") + else: + self.send_message(user_id, "Выберите вариант на кнопках ниже 👇", keyboard=get_baza_individual_keyboard()) + + def _handle_baza_help(self, user_id: int, text: str, text_lower: str): + if self._baza_nav(user_id, text_lower): + return + if "пробное" in text_lower: + self._baza_send_payment(user_id, "trial", "Пробное занятие") + return + if "подобрать" in text_lower: + self._baza_start_funnel(user_id) + return + self.send_message(user_id, "Выберите вариант на кнопках ниже 👇", keyboard=get_baza_help_keyboard()) + + def _baza_send_payment(self, user_id: int, link_key: str, tariff_label: str, campaign: str = "main"): + """Показывает ссылку на оплату/запись + кнопки «проблема с оплатой» / «вопрос менеджеру».""" + link = self._utm(config.LINKS.get(link_key, config.LINKS["tariffs"]), campaign) + db.set_profile_field(user_id, "tariff", tariff_label) + db.add_metka(user_id, "clicked_payment") + db.add_metka(user_id, "lead_hot") + db.update_user_state(user_id, "BAZA_PAY") + kb = VkKeyboard(inline=True) + kb.add_openlink_button("Перейти к оплате 💳", link) + kb.add_line() + kb.add_button("Проблема с оплатой", color=VkKeyboardColor.NEGATIVE) + kb.add_line() + kb.add_button("Задать вопрос менеджеру 💬", color=VkKeyboardColor.SECONDARY) + self.send_message( + user_id, + f"Вы выбрали: {tariff_label}.\n\n" + "Нажмите «Перейти к оплате 💳» — откроется страница оплаты. " + "После оплаты менеджер свяжется с вами и подскажет время занятия.\n\n" + "Если что-то не получается — нажмите «Проблема с оплатой», и мы поможем.", + keyboard=kb.get_keyboard() + ) + + def _handle_baza_pay(self, user_id: int, text: str, text_lower: str): + if text_lower in ["проблема с оплатой", "проблема", "не получается", "ошибка оплаты"]: + db.add_metka(user_id, "payment_problem") + self._baza_to_manager(user_id, reason="Проблема с оплатой") + return + if self._baza_nav(user_id, text_lower): + return + # Любой другой текст — считаем вопросом и передаём менеджеру + db.set_profile_field(user_id, "message", text.strip()) + self._baza_to_manager(user_id, reason="Вопрос на шаге оплаты") + + # ---------- Летний интенсив CS2 ---------- + + def _baza_start_intensive(self, user_id: int): + db.set_profile_field(user_id, "source", "Летний интенсив CS2") + db.add_metka(user_id, "bot_started") + db.add_metka(user_id, "intensive_interest") + db.update_user_state(user_id, "BAZA_INT_INFO") + self.send_message( + user_id, + config.INTENSIVE_INFO + "\n\nХотите записаться на интенсив?", + keyboard=get_baza_intensive_keyboard() + ) + + def _handle_baza_int_info(self, user_id: int, text: str, text_lower: str): + if self._baza_nav(user_id, text_lower): + return + if "участвовать" in text_lower or "записаться" in text_lower or "хочу" in text_lower or "да" == text_lower: + db.update_user_state(user_id, "BAZA_INT_AGE") + self.send_message(user_id, "Отлично! Сколько лет участнику?", keyboard=get_baza_age_keyboard()) + return + self.send_message(user_id, "Выберите действие на кнопках ниже 👇", keyboard=get_baza_intensive_keyboard()) + + def _handle_baza_int_age(self, user_id: int, text: str, text_lower: str): + if self._baza_nav(user_id, text_lower): + return + if text_lower.startswith("назад"): + db.update_user_state(user_id, "BAZA_INT_INFO") + self.send_message(user_id, "Хотите записаться на интенсив?", keyboard=get_baza_intensive_keyboard()) + return + if "младше 12" in text_lower or "меньше 12" in text_lower: + db.set_profile_field(user_id, "age", "Младше 12 лет") + db.add_metka(user_id, "age_under12") + self._baza_to_manager(user_id, reason="Интенсив: участник младше 12 лет") + return + if "12" in text_lower or "13" in text_lower: + age = "12–13 лет" + elif "14" in text_lower or "15" in text_lower: + age = "14–15 лет" + elif "16" in text_lower: + age = "16 лет и старше" + else: + self.send_message(user_id, "Выберите возраст на кнопках ниже 👇", keyboard=get_baza_age_keyboard()) + return + db.set_profile_field(user_id, "age", age) + db.update_user_state(user_id, "BAZA_INT_LEVEL") + self.send_message(user_id, "Какой сейчас уровень игры в CS2?", keyboard=get_baza_level_keyboard()) + + def _handle_baza_int_level(self, user_id: int, text: str, text_lower: str): + if self._baza_nav(user_id, text_lower): + return + if text_lower.startswith("назад"): + db.update_user_state(user_id, "BAZA_INT_AGE") + self.send_message(user_id, "Сколько лет участнику?", keyboard=get_baza_age_keyboard()) + return + levels = { + "только начинает": "Только начинает", + "играет для себя": "Играет для себя", + "есть рейтинг или ранг": "Есть рейтинг/ранг", + "играет в команде или турнирах": "Команда/турниры", + } + match = levels.get(text_lower) + if not match: + self.send_message(user_id, "Выберите уровень на кнопках ниже 👇", keyboard=get_baza_level_keyboard()) + return + db.set_profile_field(user_id, "level", match) + db.set_profile_field(user_id, "game", "CS2 (интенсив)") + self._baza_send_payment(user_id, "summer", f"Летний интенсив CS2 — {config.INTENSIVE_PRICE}", campaign="summer") + + # ---------- Цены / передача менеджеру / карточка ---------- + + def _baza_show_prices(self, user_id: int): + db.update_user_state(user_id, "FREE_CHAT") + self.send_message(user_id, config.PRICES_TEXT + "\n\nЧто дальше?", keyboard=get_baza_prices_keyboard()) + + def _baza_manager_card(self, user_id: int, reason: str = "") -> str: + p = db.get_profile(user_id) + vk_name = self._vk_name(user_id) or "—" + lines = ["🎯 Новый лид — Кибершкола БАЗА", ""] + if reason: + lines.append(f"❗ Повод: {reason}") + lines.append(f"👤 Клиент: {vk_name}") + lines.append(f"🔗 Профиль: vk.com/id{user_id}") + lines.append("") + fields = [ + ("source", "Источник"), ("role", "Для кого"), ("age", "Возраст"), + ("game", "Игра"), ("level", "Уровень"), ("format", "Формат"), + ("tariff", "Интерес / тариф"), ("message", "Сообщение"), + ] + for key, label in fields: + val = p.get(key) + if val: + lines.append(f"• {label}: {val}") + metki = p.get("metki") + if metki: + lines.append("") + lines.append("🏷 Метки: " + ", ".join(metki)) + return "\n".join(lines) + + def _baza_to_manager(self, user_id: int, reason: str = ""): + """Передаёт лид менеджеру с карточкой профиля и переводит в чат с оператором.""" + db.add_metka(user_id, "lead_manager") + card = self._baza_manager_card(user_id, reason) + self._notify_operators("💬 Запрос менеджеру\n\n" + card) + try: + mailer.send_email(f"Лид из бота БАЗА — vk.com/id{user_id}", card) + except Exception as mail_err: + logger.error(f"Не удалось отправить email о лиде: {mail_err}") + db.update_user_state(user_id, "OPERATOR_CHAT") + if self._within_work_hours(): + client_text = ("💬 Передал вас менеджеру — он ответит здесь в ближайшее время.\n" + "Можете написать свой вопрос прямо сейчас.") + else: + client_text = (f"🌙 Менеджер сейчас оффлайн (работаем с {getattr(config, 'WORK_HOUR_START', 9)}:00 " + f"до {getattr(config, 'WORK_HOUR_END', 21)}:00).\n" + "Я передал ваш запрос — ответим, как начнётся рабочий день. " + "Можете оставить сообщение здесь.") + self.send_message(user_id, client_text, keyboard=get_operator_keyboard()) + def _send_returning_prompt(self, user_id: int, user_data: Dict[str, Any]): """Приветствие для клиента, чьи имя и телефон уже сохранены.""" diff --git a/keyboards.py b/keyboards.py index 81dd859..16ce750 100644 --- a/keyboards.py +++ b/keyboards.py @@ -8,24 +8,110 @@ import config # ========================================== def get_main_keyboard(is_admin: bool = False) -> str: - """Главное меню бота. Для админа добавляется кнопка «Админка ⚙️».""" + """Главное меню Кибершколы БАЗА. Для админа добавляется кнопка «Админка ⚙️».""" keyboard = VkKeyboard(one_time=False) - # Кнопка начать опрос (зеленая) - keyboard.add_button("Заполнить заявку 📝", color=VkKeyboardColor.PRIMARY) + keyboard.add_button("Подобрать тренировку 🎯", color=VkKeyboardColor.PRIMARY) keyboard.add_line() - # Карьера в ИКП (набор игроков и сотрудников) - keyboard.add_button("Карьера в ИКП 💼", color=VkKeyboardColor.SECONDARY) + keyboard.add_button("Летний интенсив CS2 🔥", color=VkKeyboardColor.SECONDARY) keyboard.add_line() - # Кнопки FAQ и оператора - keyboard.add_button("Частые вопросы ❓", color=VkKeyboardColor.SECONDARY) - keyboard.add_button("Позвать оператора 🧑‍💼", color=VkKeyboardColor.NEGATIVE) - # Кнопка админ-панели — только для администраторов (отдельной строкой) + keyboard.add_button("Посмотреть цены 💰", color=VkKeyboardColor.SECONDARY) + keyboard.add_button("Задать вопрос 💬", color=VkKeyboardColor.SECONDARY) if is_admin: keyboard.add_line() keyboard.add_button("Админка ⚙️", color=VkKeyboardColor.POSITIVE) return keyboard.get_keyboard() +# ========================================== +# КЛАВИАТУРЫ ВОРОНКИ «ПОДОБРАТЬ ТРЕНИРОВКУ» (БАЗА) +# ========================================== + +def _kb(rows, back=False, ask_manager=False, back_menu=True): + """Хелпер: строит нижнюю клавиатуру из строк кнопок + служебные кнопки снизу. + + back — кнопка «Назад ⬅️» (к предыдущему вопросу воронки); + ask_manager — кнопка «Задать вопрос менеджеру 💬»; + back_menu — кнопка «В главное меню ↩️». + """ + keyboard = VkKeyboard(one_time=False) + for i, row in enumerate(rows): + if i: + keyboard.add_line() + for label in row: + keyboard.add_button(label, color=VkKeyboardColor.SECONDARY) + if ask_manager: + keyboard.add_line() + keyboard.add_button("Задать вопрос менеджеру 💬", color=VkKeyboardColor.SECONDARY) + if back or back_menu: + keyboard.add_line() + if back: + keyboard.add_button("Назад ⬅️", color=VkKeyboardColor.SECONDARY) + if back_menu: + keyboard.add_button("В главное меню ↩️", color=VkKeyboardColor.SECONDARY) + return keyboard.get_keyboard() + + +def get_baza_forwho_keyboard() -> str: + return _kb([["Для себя", "Для подростка"]]) + + +def get_baza_age_keyboard() -> str: + return _kb([["12–13 лет", "14–15 лет"], ["16 лет и старше", "Младше 12 лет"]], back=True) + + +def get_baza_younger12_keyboard() -> str: + keyboard = VkKeyboard(one_time=False) + keyboard.add_button("Обсудить с менеджером 💬", color=VkKeyboardColor.SECONDARY) + keyboard.add_line() + keyboard.add_button("Назад ⬅️", color=VkKeyboardColor.SECONDARY) + keyboard.add_button("В главное меню ↩️", color=VkKeyboardColor.SECONDARY) + return keyboard.get_keyboard() + + +def get_baza_game_keyboard() -> str: + return _kb([["CS2", "Dota 2"], ["World of Tanks", "Другая игра"]], back=True) + + +def get_baza_level_keyboard() -> str: + return _kb([["Только начинает"], ["Играет для себя"], + ["Есть рейтинг или ранг"], ["Играет в команде или турнирах"]], back=True) + + +def get_baza_format_keyboard() -> str: + return _kb([["Групповые тренировки"], ["Индивидуальные тренировки"], + ["Пока не знаю, нужен совет"]], back=True) + + +def get_baza_group_keyboard() -> str: + return _kb([["Записаться на пробное"], ["Купить 4 тренировки", "Купить 8 тренировок"]], + back=True, ask_manager=True) + + +def get_baza_individual_keyboard() -> str: + return _kb([["Записаться на пробное"], ["Купить 1 тренировку"], + ["Купить 4 тренировки", "Купить 8 тренировок"]], + back=True, ask_manager=True) + + +def get_baza_help_keyboard() -> str: + return _kb([["Записаться на пробное"], ["Подобрать заново 🎯"]], ask_manager=True) + + +def get_baza_intensive_keyboard() -> str: + return _kb([["Хочу участвовать 🔥"]], ask_manager=True) + + +def get_baza_prices_keyboard() -> str: + keyboard = VkKeyboard(one_time=False) + keyboard.add_button("Подобрать тренировку 🎯", color=VkKeyboardColor.PRIMARY) + keyboard.add_line() + keyboard.add_button("Записаться на пробное", color=VkKeyboardColor.SECONDARY) + keyboard.add_button("Выбрать абонемент", color=VkKeyboardColor.SECONDARY) + keyboard.add_line() + keyboard.add_button("Задать вопрос 💬", color=VkKeyboardColor.SECONDARY) + return keyboard.get_keyboard() + + def get_admin_keyboard(is_super: bool = False) -> str: """Клавиатура админ-панели с разделами. Супер-админу — раздел «Администраторы».""" keyboard = VkKeyboard(one_time=False) diff --git a/tests/test_flow.py b/tests/test_flow.py index 0c3de59..02ea176 100644 --- a/tests/test_flow.py +++ b/tests/test_flow.py @@ -5,26 +5,83 @@ def _state(uid): return bot.db.get_user_data(uid)["state"] -def test_full_lead_flow(app): +def _prof(uid): + return bot.db.get_profile(uid) + + +def test_baza_group_funnel_to_payment(app): + """Полный подбор → групповой абонемент → шаг оплаты (BAZA_PAY).""" u = 70001 - assert (app.handle_message(u, "Заполнить заявку 📝"), _state(u))[1] == "FORM_NAME" - app.handle_message(u, "Иван"); assert _state(u) == "FORM_PHONE" - app.handle_message(u, "89990001122"); assert _state(u) == "FORM_QUESTION" - app.handle_message(u, "кс"); assert _state(u) == "FORM_CALLDATE" - app.handle_message(u, "Завтра"); assert _state(u) == "FORM_CALLTIME" - app.handle_message(u, "14:00"); assert _state(u) == "FORM_REVIEW" - app.handle_message(u, "Да, всё верно! ✅"); assert _state(u) == "FORM_CONSENT_1" - app.handle_message(u, "Согласен(а) и подтверждаю ✅"); assert _state(u) == "FORM_CONSENT_2" - app.handle_message(u, "Без рассылки ➡️"); assert _state(u) == "FREE_CHAT" - stats = bot.db.get_lead_stats() - assert stats["total"] == 1 and stats["from_bot"] == 1 + app.handle_message(u, "Подобрать тренировку 🎯"); assert _state(u) == "BAZA_FORWHO" + app.handle_message(u, "Для подростка"); assert _state(u) == "BAZA_AGE" + app.handle_message(u, "14–15 лет"); assert _state(u) == "BAZA_GAME" + app.handle_message(u, "CS2"); assert _state(u) == "BAZA_LEVEL" + app.handle_message(u, "Играет для себя"); assert _state(u) == "BAZA_FORMAT" + app.handle_message(u, "Групповые тренировки"); assert _state(u) == "BAZA_GROUP" + app.handle_message(u, "Купить 4 тренировки"); assert _state(u) == "BAZA_PAY" + p = _prof(u) + assert p["role"].startswith("Для подростка") and p["game"] == "CS2" + assert p["format"] == "Групповые тренировки" and "4" in p["tariff"] + assert {"role_parent", "game_cs2", "format_group", "clicked_payment", "lead_hot"} <= set(p["metki"]) -def test_career_player_flow(app): - u = 70003 - app.handle_message(u, "Карьера в ИКП 💼"); assert _state(u) == "VAC_CHOOSE" - app.handle_message(u, "Игроком в состав 🎮"); assert _state(u) == "VAC_P_DISCIPLINE" - app.handle_message(u, "Counter-Strike 2 🔫"); assert _state(u) == "VAC_P_RANK" +def test_baza_individual_and_back(app): + """Индивидуальная ветка + кнопка «Назад» возвращает на предыдущий шаг.""" + u = 70011 + app.handle_message(u, "Подобрать тренировку 🎯") + app.handle_message(u, "Для себя"); assert _state(u) == "BAZA_AGE" + app.handle_message(u, "16 лет и старше"); assert _state(u) == "BAZA_GAME" + app.handle_message(u, "Назад ⬅️"); assert _state(u) == "BAZA_AGE" + app.handle_message(u, "16 лет и старше") + app.handle_message(u, "Dota 2"); assert _state(u) == "BAZA_LEVEL" + app.handle_message(u, "Есть рейтинг или ранг") + app.handle_message(u, "Индивидуальные тренировки"); assert _state(u) == "BAZA_INDIVIDUAL" + app.handle_message(u, "Купить 8 тренировок"); assert _state(u) == "BAZA_PAY" + assert "8" in _prof(u)["tariff"] + + +def test_baza_under12_goes_to_manager(app): + u = 70012 + app.handle_message(u, "Подобрать тренировку 🎯") + app.handle_message(u, "Для подростка") + app.handle_message(u, "Младше 12 лет"); assert _state(u) == "BAZA_YOUNG" + app.handle_message(u, "Обсудить с менеджером 💬") + assert _state(u) == "OPERATOR_CHAT" + assert "age_under12" in _prof(u)["metki"] + + +def test_baza_other_game_goes_to_manager(app): + u = 70013 + app.handle_message(u, "Подобрать тренировку 🎯") + app.handle_message(u, "Для себя") + app.handle_message(u, "16 лет и старше") + app.handle_message(u, "Другая игра"); assert _state(u) == "BAZA_GAME_OTHER" + app.handle_message(u, "Valorant"); assert _state(u) == "OPERATOR_CHAT" + assert "Valorant" in _prof(u)["game"] + + +def test_baza_intensive_by_keyword(app): + """Кодовое слово «лето» запускает интенсив; ветка доходит до оплаты.""" + u = 70014 + app.handle_message(u, "лето"); assert _state(u) == "BAZA_INT_INFO" + app.handle_message(u, "Хочу участвовать 🔥"); assert _state(u) == "BAZA_INT_AGE" + app.handle_message(u, "14–15 лет"); assert _state(u) == "BAZA_INT_LEVEL" + app.handle_message(u, "Играет в команде или турнирах"); assert _state(u) == "BAZA_PAY" + assert "intensive_interest" in _prof(u)["metki"] + + +def test_baza_question_button_handoff(app): + u = 70015 + app.handle_message(u, "Задать вопрос 💬"); assert _state(u) == "OPERATOR_CHAT" + assert "lead_manager" in _prof(u)["metki"] + + +def test_baza_prices_and_menu(app): + u = 70016 + app._sent.clear() + app.handle_message(u, "Посмотреть цены 💰") + assert _state(u) == "FREE_CHAT" + assert any("2 790" in s["msg"] for s in app._sent) def test_flood_does_not_crash(app):