From c97cf5e0d8a292598b0ffaba8a3bc3ea7e43c182 Mon Sep 17 00:00:00 2001 From: "oleg.vodyanov91@gmail.com" Date: Sun, 8 Jun 2025 22:32:01 +0300 Subject: [PATCH] add verification logic --- src/core/enums/dialog_helpers.py | 10 ++ src/core/texts.py | 2 + src/db/models.py | 8 +- src/docbot/handlers/admins/verify_handler.py | 166 +++++++++++++++++++ src/docbot/main.py | 2 + src/docbot/services/admins_service.py | 60 ++++++- 6 files changed, 242 insertions(+), 6 deletions(-) create mode 100644 src/core/texts.py create mode 100644 src/docbot/handlers/admins/verify_handler.py diff --git a/src/core/enums/dialog_helpers.py b/src/core/enums/dialog_helpers.py index 03a032f..b72325b 100644 --- a/src/core/enums/dialog_helpers.py +++ b/src/core/enums/dialog_helpers.py @@ -9,3 +9,13 @@ class ConfirmationMessage(str, Enum): class Acknowledgement(str, Enum): OK = "Понятно 😊" DECLINE = "Отмена 😔" + + +class YesNo(str, Enum): + YES = "Да 😊" + NO = "Нет 😔" + + +class Confirmation(str, Enum): + APPROVE = "✅ Подтвердить" + REJECT = "❌ Отклонить" diff --git a/src/core/texts.py b/src/core/texts.py new file mode 100644 index 0000000..8c8be92 --- /dev/null +++ b/src/core/texts.py @@ -0,0 +1,2 @@ + +NO_PERMISSIONS_TO_USE_COMMAND = "❌ У вас нет прав для этой команды." diff --git a/src/db/models.py b/src/db/models.py index 5802edb..74fbd79 100644 --- a/src/db/models.py +++ b/src/db/models.py @@ -61,10 +61,9 @@ class Doctors(Base): back_populates="doctor", cascade="all, delete-orphan") verification_requests: Mapped[List["VerificationRequests"]] = relationship( back_populates="doctor", cascade="all, delete-orphan") - def __repr__(self) -> str: - return f"" + return f"" class ReferralCode(Base): @@ -93,10 +92,11 @@ class VerificationRequests(Base): reviewed_at: Mapped[Optional[datetime]] = mapped_column(nullable=True) status: Mapped[str] = mapped_column(default=False, nullable=False) # Связь поправлена — связь с doctor через doctor_id - doctor: Mapped["Doctors"] = relationship(back_populates="verification_requests") + doctor: Mapped["Doctors"] = relationship( + back_populates="verification_requests") def __repr__(self) -> str: - return f"" + return f"" class FormLink(Base): diff --git a/src/docbot/handlers/admins/verify_handler.py b/src/docbot/handlers/admins/verify_handler.py new file mode 100644 index 0000000..03a03e9 --- /dev/null +++ b/src/docbot/handlers/admins/verify_handler.py @@ -0,0 +1,166 @@ +from telegram import Update +from telegram.ext import ( + ContextTypes, CommandHandler, ConversationHandler, CallbackQueryHandler, + MessageHandler, filters +) + +from core.logging import logger +from core.enums.dialog_helpers import Confirmation +from core.texts import NO_PERMISSIONS_TO_USE_COMMAND +from docbot.services.admins_service import ( + get_admin_info, get_doctors_waiting_authorization, approve_doctor, + reject_doctor, get_payment_link +) +from telegram import InlineKeyboardButton, InlineKeyboardMarkup + + +SELECT_DOCTOR, VERIFY_DOCTOR, SEND_PAYMENT_LINK = range(3) +NO_DOCTORS = "☹️ Нет врачей, ожидающих авторизации." +SELECT_DOCTOR_TO_VERIFY = "👀 Выберите врача для авторизации:" +CHOOSE_DOCTORS_DESTINY = "🙄 Врач с ID {doctor_telegram_id}. Что сделать?" +VERIFIED = "Врач с ID {doctor_telegram_id} верифицирован ✅" +REJECTED = "Верификация врача с ID {doctor_telegram_id} отклонена ❌" +PAYMENT_LINK = "Ссылка на оплату для врача с ID {doctor_telegram_id}: {payment_link}" + + +async def verify(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """ + Команда /verify — Проверка верификации врача. + Доступна только администратору чата. + """ + user_id = update.effective_user.id + if not await get_admin_info(user_id): + logger.warning( + f"Пользователь {user_id} пытался проверить верификацию без прав.") + await update.message.reply_text(NO_PERMISSIONS_TO_USE_COMMAND) + return + + # Здесь должна быть логика проверки верификации врача + # Например, можно запросить информацию о врачах и их статусе верификации + logger.info(f"Admin {user_id} запрашивает список врачей для верификации.") + doctors = await get_doctors_waiting_authorization() + if not doctors: + await update.message.reply_text(NO_DOCTORS) + return + + keyboard = [ + [ + InlineKeyboardButton( + text=doctor.Doctors.name + " " + doctor.VerificationRequests.code, + callback_data=f"verify_{doctor.Doctors.telegram_id}" + ) + ] for doctor in doctors + ] + + reply_markup = InlineKeyboardMarkup(keyboard) + + await update.message.reply_text( + SELECT_DOCTOR_TO_VERIFY, + reply_markup=reply_markup + ) + + return SELECT_DOCTOR + + +async def doctor_selected(update: Update, context: ContextTypes.DEFAULT_TYPE): + query = update.callback_query + await query.answer() + + # Получаем ID врача из callback_data + doctor_telegram_id = int(query.data.split("_")[1]) + # (Можно получить объект врача по doctor_id из базы, если надо) + + # Кнопки "Верифицировать" и "Отклонить" + keyboard = [ + [ + InlineKeyboardButton( + Confirmation.APPROVE.value, + callback_data=f"approve_{doctor_telegram_id}" + ), + InlineKeyboardButton( + Confirmation.REJECT.value, + callback_data=f"reject_{doctor_telegram_id}" + ), + ] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text( + CHOOSE_DOCTORS_DESTINY.format(doctor_telegram_id=doctor_telegram_id), + reply_markup=reply_markup, + ) + + return VERIFY_DOCTOR + + +async def doctor_action(update: Update, context: ContextTypes.DEFAULT_TYPE): + query = update.callback_query + await query.answer() + data = query.data.split("_") + action = data[0] # approve или reject + doctor_telegram_id = int(data[1]) + context.user_data['doctor_telegram_id'] = doctor_telegram_id + + if action == "approve": + # Здесь твоя логика верификации врача (например, запись в базу) + approve_doctor(doctor_telegram_id) + logger.info(f"Врач с ID {doctor_telegram_id} верифицирован.") + # Отправляем сообщение об успешной верификации + await query.edit_message_text( + VERIFIED.format(doctor_telegram_id=doctor_telegram_id) + ) + elif action == "reject": + # Логика отклонения + reject_doctor(doctor_telegram_id) + logger.info(f"Врач с ID {doctor_telegram_id} отклонен.") + # Отправляем сообщение об отклонении + await query.edit_message_text( + REJECTED.format(doctor_telegram_id=doctor_telegram_id) + ) + + return SEND_PAYMENT_LINK + + +def send_payment_link(update: Update, context: ContextTypes.DEFAULT_TYPE): + """ + Отправляет ссылку на оплату после верификации врача. + """ + doctor_telegram_id = context.user_data.get('doctor_telegram_id') + if not doctor_telegram_id: + logger.error("Не удалось получить ID врача из user_data.") + return + + # Здесь должна быть логика получения ссылки на оплату + payment_link = get_payment_link(doctor_telegram_id) + + # Отправляем ссылку на оплату + update.message.reply_text( + PAYMENT_LINK.format( + doctor_telegram_id=doctor_telegram_id, + payment_link=payment_link + ) + ) + + return ConversationHandler.END + + +def get_verify_handler(): + """ + Возвращает список готовых CommandHandler-ов для регистрации в Application. + """ + return ConversationHandler( + entry_points=[CommandHandler("verify", verify)], + states={ + SELECT_DOCTOR: [ + CallbackQueryHandler(doctor_selected, pattern=r"^verify_\d+$"), + ], + VERIFY_DOCTOR: [ + CallbackQueryHandler( + doctor_action, pattern=r"^(approve|reject)_\d+$"), + ], + SEND_PAYMENT_LINK: [ + MessageHandler(filters.TEXT & ~filters.COMMAND, + send_payment_link), + ], + }, + fallbacks=[], + ) diff --git a/src/docbot/main.py b/src/docbot/main.py index e47b74b..227f182 100644 --- a/src/docbot/main.py +++ b/src/docbot/main.py @@ -9,6 +9,7 @@ from docbot.handlers.doctors.register_handler import get_register_doctor_first_s from docbot.handlers.admins.generate_ref import get_referral_handlers from docbot.handlers.utils.help import get_help_handler from docbot.handlers.utils.unknown import get_unknown_handler +from docbot.handlers.admins.verify_handler import get_verify_handler def main(): @@ -27,6 +28,7 @@ def main(): app.add_handler(get_doctors_handler()) app.add_handler(get_register_doctor_first_stage_handler()) app.add_handler(get_referral_handlers()) + app.add_handler(get_verify_handler()) app.add_handler(get_help_handler()) app.add_handler(get_unknown_handler()) logger.debug("Все хэндлеры зарегистрированы, запускаем polling") diff --git a/src/docbot/services/admins_service.py b/src/docbot/services/admins_service.py index 13b27dd..f6274fc 100644 --- a/src/docbot/services/admins_service.py +++ b/src/docbot/services/admins_service.py @@ -1,8 +1,8 @@ from __future__ import annotations -from sqlalchemy import select +from sqlalchemy import select, Sequence, Row, Tuple from db.session import AsyncSessionLocal -from db.models import Admins +from db.models import Admins, Doctors, VerificationRequests async def get_admin_info(telegram_id: int) -> Admins | None: @@ -21,3 +21,59 @@ async def mark_doctor_inactive(telegram_id: int) -> Admins | None: .where(Admins.telegram_id == telegram_id) ) return result.scalar_one_or_none() + + +async def get_doctors_waiting_authorization() -> Sequence[Row[Tuple[VerificationRequests, Doctors]]] | None: + async with AsyncSessionLocal() as session: + result = await session.execute( + select(VerificationRequests, Doctors) + .join(Doctors, VerificationRequests.doctor_id == Doctors.id) + .where(Doctors.is_verified.is_(False), Doctors.is_active.is_(False)) + .order_by(VerificationRequests.sent_at.desc()) + ) + + return result.columns(VerificationRequests, Doctors).all() + + +async def approve_doctor(doctor_id: int) -> bool: + async with AsyncSessionLocal() as session: + async with session.begin(): + result = await session.execute( + select(Doctors).where(Doctors.telegram_id == doctor_id) + ) + doctor = result.scalar_one_or_none() + if not doctor: + return False + + doctor.is_verified = True + doctor.is_active = True + return True + + +async def reject_doctor(doctor_id: int) -> bool: + async with AsyncSessionLocal() as session: + async with session.begin(): + result = await session.execute( + select(Doctors).where(Doctors.telegram_id == doctor_id) + ) + doctor = result.scalar_one_or_none() + if not doctor: + return False + + doctor.is_verified = False + doctor.is_active = False + return True + + +async def get_payment_methods(admin_telegram_id: int) -> list[str] | None: + """ + Получает список доступных методов оплаты для врача. + Возвращает список строк или None, если нет методов. + """ + async with AsyncSessionLocal() as session: + result = await session.execute( + select(Admins.available_payment_methods) + .where(Admins.telegram_id == admin_telegram_id) + ) + payment_methods = result.scalar_one_or_none() + return payment_methods if payment_methods else []