diff --git a/.gitignore b/.gitignore index b65d279..fb520dc 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ alembic/ db/ !src/db __pycache__ -src/docbot/conversations.pkl \ No newline at end of file +src/docbot/conversations.pkl +conversations.pkl \ No newline at end of file diff --git a/src/core/enums/statuses_helpers.py b/src/core/enums/statuses_helpers.py new file mode 100644 index 0000000..037f12f --- /dev/null +++ b/src/core/enums/statuses_helpers.py @@ -0,0 +1,7 @@ +from enum import Enum + + +class ObjectStatuses(str, Enum): + SENT = "Sent" + DECLINED = "Declined" + ACCEPTED = "Accepted" diff --git a/src/core/utils.py b/src/core/utils.py new file mode 100644 index 0000000..c300c1e --- /dev/null +++ b/src/core/utils.py @@ -0,0 +1,9 @@ +import uuid + + +def UUID_code_generator() -> str: + """ + Генерирует уникальный код в формате UUID. + Возвращает строку с кодом. + """ + return str(uuid.uuid4().hex[:8]) diff --git a/src/db/models.py b/src/db/models.py index beae98f..5802edb 100644 --- a/src/db/models.py +++ b/src/db/models.py @@ -1,7 +1,7 @@ from __future__ import annotations from datetime import datetime from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship -from sqlalchemy import String, DateTime, Boolean, ForeignKey, Integer, UniqueConstraint +from sqlalchemy import String, ForeignKey from sqlalchemy.dialects.postgresql import UUID, ARRAY import uuid from typing import List, Optional @@ -14,41 +14,54 @@ class Base(DeclarativeBase): class SessionCode(Base): __tablename__ = "session_codes" - id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) code: Mapped[str] = mapped_column(String(8), unique=True, nullable=False) patient_telegram_id: Mapped[int] = mapped_column(nullable=False) sent_at: Mapped[datetime] = mapped_column(nullable=False) consulted_at: Mapped[Optional[datetime]] = mapped_column(nullable=True) - doctor_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("doctors.id")) + doctor_id: Mapped[Optional[uuid.UUID]] = mapped_column( + UUID(as_uuid=True), ForeignKey("doctors.id")) class Admins(Base): __tablename__ = "admins" - id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) telegram_id: Mapped[int] = mapped_column(unique=True, nullable=False) created_at: Mapped[Optional[datetime]] = mapped_column(nullable=True) - available_payment_methods: Mapped[Optional[List[str]]] = mapped_column(ARRAY(String), nullable=True) + available_payment_methods: Mapped[Optional[List[str]]] = mapped_column( + ARRAY(String), nullable=True) class Doctors(Base): __tablename__ = "doctors" - id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) telegram_id: Mapped[int] = mapped_column(unique=True, nullable=False) - code: Mapped[str] = mapped_column(String(8), unique=True, nullable=False) name: Mapped[str] = mapped_column(nullable=False) - available_formats: Mapped[Optional[List[str]]] = mapped_column(ARRAY(String), nullable=True) + available_formats: Mapped[Optional[List[str]] + ] = mapped_column(ARRAY(String), nullable=True) is_active: Mapped[bool] = mapped_column(default=False, nullable=False) is_verified: Mapped[bool] = mapped_column(default=False, nullable=False) created_at: Mapped[datetime] = mapped_column(nullable=False) - referral_code_id: Mapped[int] = mapped_column(ForeignKey("referral_codes.id"), nullable=False) - specialties: Mapped[List[str]] = mapped_column(ARRAY(String), nullable=False, default=list) + referral_code_id: Mapped[int] = mapped_column( + ForeignKey("referral_codes.id"), nullable=False) + specialties: Mapped[List[str]] = mapped_column( + ARRAY(String), nullable=False, default=list) - referral: Mapped["ReferralCode"] = relationship(back_populates="doctor", uselist=False) + referral: Mapped["ReferralCode"] = relationship( + back_populates="doctor", uselist=False) session_codes: Mapped[List["SessionCode"]] = relationship() - form_links: Mapped[List["FormLink"]] = relationship(back_populates="doctor", cascade="all, delete-orphan") - payment_methods: Mapped[List["PaymentMethod"]] = relationship(back_populates="doctor", cascade="all, delete-orphan") + form_links: Mapped[List["FormLink"]] = relationship( + back_populates="doctor", cascade="all, delete-orphan") + payment_methods: Mapped[List["PaymentMethod"]] = relationship( + 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"" @@ -62,7 +75,8 @@ class ReferralCode(Base): is_used: Mapped[bool] = mapped_column(default=False, nullable=False) created_at: Mapped[datetime] = mapped_column(nullable=False) used_at: Mapped[Optional[datetime]] = mapped_column(nullable=True) - doctor: Mapped[Optional["Doctors"]] = relationship(back_populates="referral", uselist=False) + doctor: Mapped[Optional["Doctors"]] = relationship( + back_populates="referral", uselist=False) def __repr__(self) -> str: return f"" @@ -72,12 +86,14 @@ class VerificationRequests(Base): __tablename__ = "verification_requests" id: Mapped[int] = mapped_column(primary_key=True) - doctor_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("doctors.id", ondelete="CASCADE"), nullable=False) + doctor_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey( + "doctors.id", ondelete="CASCADE"), nullable=False) code: Mapped[str] = mapped_column(unique=True, nullable=False) sent_at: Mapped[datetime] = mapped_column(nullable=False) 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() + doctor: Mapped["Doctors"] = relationship(back_populates="verification_requests") def __repr__(self) -> str: return f"" @@ -87,7 +103,8 @@ class FormLink(Base): __tablename__ = "form_links" id: Mapped[int] = mapped_column(primary_key=True) - doctor_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("doctors.id", ondelete="CASCADE"), nullable=False) + doctor_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey( + "doctors.id", ondelete="CASCADE"), nullable=False) url: Mapped[str] = mapped_column(nullable=False) label: Mapped[Optional[str]] = mapped_column(nullable=True) is_active: Mapped[bool] = mapped_column(default=True, nullable=False) @@ -100,10 +117,11 @@ class PaymentMethod(Base): __tablename__ = "payment_methods" id: Mapped[int] = mapped_column(primary_key=True) - doctor_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("doctors.id", ondelete="CASCADE"), nullable=False) + doctor_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey( + "doctors.id", ondelete="CASCADE"), nullable=False) method: Mapped[str] = mapped_column(nullable=False) details: Mapped[Optional[str]] = mapped_column(nullable=True) is_active: Mapped[bool] = mapped_column(default=True, nullable=False) created_at: Mapped[datetime] = mapped_column(nullable=False) - doctor: Mapped["Doctors"] = relationship(back_populates="payment_methods") \ No newline at end of file + doctor: Mapped["Doctors"] = relationship(back_populates="payment_methods") diff --git a/src/docbot/handlers/doctors/register_handler.py b/src/docbot/handlers/doctors/register_handler.py index a5ba083..bd38f72 100644 --- a/src/docbot/handlers/doctors/register_handler.py +++ b/src/docbot/handlers/doctors/register_handler.py @@ -8,8 +8,10 @@ from telegram.ext import ( ) from docbot.handlers.utils.cancel_handler import get_cancel_handler -from docbot.services.referral_service import validate_referral_code, mark_referral_code_as_used +from docbot.services.referral_service import validate_referral_code +from docbot.services.doctors_service import create_doctor from core.enums.dialog_helpers import ConfirmationMessage, Acknowledgement +from core.utils import UUID_code_generator ASK_REFERRAL_CODE = 1 SEND_ACKNOWLEDGEMENT_INFO = 2 @@ -32,7 +34,7 @@ SEND_ME_YOUR_SPECIALITY_TEXT = ( "📝 Пожалуйста, введите вашу специальность, в соответствии с которой планируете проводить консультации." ) WAIT_FOR_ACTIVATION_TEXT = ( - "📝 Заявка принята, направьте диплом и аккредитацию с указанием кода в теме письма на адрес электронной почты:____\n" + "📝 Заявка принята, направьте диплом и аккредитацию с указанием кода верификации {0} в теме письма на адрес электронной почты: docbot@docbot.ru\n" "В этом шаге нужно встроить подсказку: в каком формате направлять диплом, как скачать с госуслуг\n" "выписку об аккредитации. Верификация займет 24 часа." ) @@ -82,7 +84,7 @@ async def receive_doctor_referral_code(update: Update, context: ContextTypes.DEF async def receive_doctor_info_confirmation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: doctor_info_confirmation = update.message.text.strip() - if doctor_info_confirmation == ConfirmationMessage.DECLINE.value: + if doctor_info_confirmation != ConfirmationMessage.PROCEED.value: return ConversationHandler.END await update.message.reply_text( @@ -114,8 +116,11 @@ async def receive_doctor_speciality(update: Update, context: ContextTypes.DEFAUL ] ] + code = UUID_code_generator() + context.user_data["verification_request_code"] = code + await update.message.reply_text( - WAIT_FOR_ACTIVATION_TEXT, + WAIT_FOR_ACTIVATION_TEXT.format(code), parse_mode="Markdown", reply_markup=ReplyKeyboardMarkup( reply_keyboard, one_time_keyboard=True, input_field_placeholder="Отправка пакета документов" @@ -160,10 +165,11 @@ async def receive_doctor_consultation_packages_acknowledgement_status(update: Up parse_mode="Markdown" ) - await mark_referral_code_as_used( + await create_doctor( context.user_data["ref_obj"], context.user_data["doctor_telegram_id"], - context.user_data["doctor_name"] + context.user_data["doctor_name"], + context.user_data["verification_request_code"] ) return ConversationHandler.END @@ -178,19 +184,24 @@ def get_register_doctor_first_stage_handler() -> ConversationHandler: entry_points=[ask_doctor_info_handler()], states={ ASK_REFERRAL_CODE: [ - MessageHandler(filters.TEXT & ~filters.COMMAND, receive_doctor_referral_code) + MessageHandler(filters.TEXT & ~filters.COMMAND, + receive_doctor_referral_code) ], SEND_ACKNOWLEDGEMENT_INFO: [ - MessageHandler(filters.TEXT & ~filters.COMMAND, receive_doctor_info_confirmation) + MessageHandler(filters.TEXT & ~filters.COMMAND, + receive_doctor_info_confirmation) ], ASK_NAME: [ - MessageHandler(filters.TEXT & ~filters.COMMAND, receive_doctor_name) + MessageHandler(filters.TEXT & ~filters.COMMAND, + receive_doctor_name) ], ASK_SPECIALITY: [ - MessageHandler(filters.TEXT & ~filters.COMMAND, receive_doctor_speciality) + MessageHandler(filters.TEXT & ~filters.COMMAND, + receive_doctor_speciality) ], SEND_DIPLOMA_ACK_INFO: [ - MessageHandler(filters.TEXT & ~filters.COMMAND, receive_doctor_diploma_acknowledgement_status) + MessageHandler(filters.TEXT & ~filters.COMMAND, + receive_doctor_diploma_acknowledgement_status) ], SEND_CONSULTATION_TYPE_ACK_INFO: [ MessageHandler(filters.TEXT & ~filters.COMMAND, diff --git a/src/docbot/handlers/utils/help.py b/src/docbot/handlers/utils/help.py index f7cb2eb..322e186 100644 --- a/src/docbot/handlers/utils/help.py +++ b/src/docbot/handlers/utils/help.py @@ -1,9 +1,10 @@ from telegram import Update, ReplyKeyboardMarkup -from telegram.ext import ContextTypes +from telegram.ext import ContextTypes, CommandHandler from docbot.services.admins_service import get_admin_info from docbot.services.doctors_service import get_doctor_info + async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """ Отдаёт список команд, доступных текущему пользователю: @@ -57,4 +58,11 @@ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No one_time_keyboard=True, resize_keyboard=True ) - await update.message.reply_text(text, parse_mode="Markdown", reply_markup=keyboard) \ No newline at end of file + await update.message.reply_text(text, parse_mode="Markdown", reply_markup=keyboard) + + +def get_help_handler() -> CommandHandler: + """ + Возвращает хэндлер для команды /help. + """ + return CommandHandler("help", help_command) diff --git a/src/docbot/main.py b/src/docbot/main.py index cae516c..e47b74b 100644 --- a/src/docbot/main.py +++ b/src/docbot/main.py @@ -7,6 +7,7 @@ from docbot.handlers.patients.send_form_handler import get_send_form_handler from docbot.handlers.admins.doctors_handler import get_doctors_handler from docbot.handlers.doctors.register_handler import get_register_doctor_first_stage_handler 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 @@ -26,6 +27,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_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 7263efc..13b27dd 100644 --- a/src/docbot/services/admins_service.py +++ b/src/docbot/services/admins_service.py @@ -9,7 +9,7 @@ async def get_admin_info(telegram_id: int) -> Admins | None: async with AsyncSessionLocal() as session: result = await session.execute( select(Admins.telegram_id) - .where(Admins.telegram_id.match(telegram_id)) + .where(Admins.telegram_id == telegram_id) ) return result.scalar_one_or_none() @@ -18,6 +18,6 @@ async def mark_doctor_inactive(telegram_id: int) -> Admins | None: async with AsyncSessionLocal() as session: result = await session.execute( select(Admins.telegram_id) - .where(Admins.telegram_id.match(telegram_id)) + .where(Admins.telegram_id == telegram_id) ) - return result.scalar_one_or_none() \ No newline at end of file + return result.scalar_one_or_none() diff --git a/src/docbot/services/doctors_service.py b/src/docbot/services/doctors_service.py index ef4d85f..6943557 100644 --- a/src/docbot/services/doctors_service.py +++ b/src/docbot/services/doctors_service.py @@ -5,15 +5,16 @@ from datetime import datetime from sqlalchemy import select from db.session import AsyncSessionLocal -from db.models import Doctors +from db.models import Doctors, VerificationRequests, ReferralCode from core.enums.consultation_types import Consultation +from core.enums.statuses_helpers import ObjectStatuses async def get_doctor_info(telegram_id: int) -> Doctors | None: async with AsyncSessionLocal() as session: result = await session.execute( select(Doctors) - .where(Doctors.telegram_id.match(telegram_id)) + .where(Doctors.telegram_id == telegram_id) ) return result.scalar_one_or_none() @@ -37,3 +38,35 @@ async def add_doctor(telegram_id: int, name: str, available_formats: Consultatio is_active=is_active, created_at=datetime.utcnow() )) + + +async def create_doctor(referral_obj: ReferralCode, telegram_id: int, name: str, verification_code: str) -> Doctors: + """ + Помечает referral_obj как использованный, создаёт запись в таблице Doctors + с привязкой к telegram_id и возвращает объект Doctor. + """ + async with AsyncSessionLocal() as session: + async with session.begin(): + # помечаем код + referral_obj.is_used = True + referral_obj.used_at = datetime.utcnow() + + # создаём врача + new_doc = Doctors( + telegram_id=telegram_id, + name=name, + is_active=False, + created_at=datetime.utcnow(), + referral=referral_obj + ) + + verification_request = VerificationRequests() + verification_request.code = verification_code + verification_request.requested_at = datetime.utcnow() + verification_request.status = ObjectStatuses.SENT.value + verification_request.doctor = new_doc + verification_request.sent_at = datetime.utcnow() + session.add(verification_request) + session.add(new_doc) + await session.commit() + return new_doc diff --git a/src/docbot/services/referral_service.py b/src/docbot/services/referral_service.py index dacf7a6..b8a2cfa 100644 --- a/src/docbot/services/referral_service.py +++ b/src/docbot/services/referral_service.py @@ -15,7 +15,8 @@ async def generate_referral_code(length: int = 12) -> str: """ alphabet = string.ascii_uppercase + string.digits while True: - referral_code = "".join(secrets.choice(alphabet) for _ in range(length)) + referral_code = "".join(secrets.choice(alphabet) + for _ in range(length)) async with AsyncSessionLocal() as session: result = await session.execute( select(ReferralCode) @@ -23,7 +24,8 @@ async def generate_referral_code(length: int = 12) -> str: ) exists = result.scalar_one_or_none() if not exists: - new_ref = ReferralCode(code=referral_code, created_at=datetime.utcnow()) + new_ref = ReferralCode( + code=referral_code, created_at=datetime.utcnow()) session.add(new_ref) await session.commit() return referral_code @@ -43,28 +45,3 @@ async def validate_referral_code(referral_code: str) -> ReferralCode | None: ) ) return result.scalar_one_or_none() - - -async def mark_referral_code_as_used(referral_obj: ReferralCode, telegram_id: int, name: str) -> Doctors: - """ - Помечает referral_obj как использованный, создаёт запись в таблице Doctors - с привязкой к telegram_id и возвращает объект Doctor. - """ - async with AsyncSessionLocal() as session: - async with session.begin(): - # помечаем код - referral_obj.is_used = True - referral_obj.used_at = datetime.utcnow() - - # создаём врача - new_doc = Doctors( - telegram_id=telegram_id, - name=name, - is_active=False, - created_at=datetime.utcnow(), - referral=referral_obj - ) - session.add(new_doc) - await session.commit() - return new_doc - diff --git a/src/docbot/services/session_service.py b/src/docbot/services/session_service.py index d910f69..bd2fd4a 100644 --- a/src/docbot/services/session_service.py +++ b/src/docbot/services/session_service.py @@ -7,15 +7,16 @@ from sqlalchemy import select from db.session import AsyncSessionLocal from db.models import SessionCode +from core.utils import UUID_code_generator async def create_session_code(telegram_id: int, form_link: str) -> str: """ - Генерирует уникальный код, сохраняет его вместе с Telegram ID и ссылкой на анкету. + Генерирует уникальный код, сохраняет его вместе с Telegram ID. Возвращает этот код. """ - code = uuid.uuid4().hex[:8] - async with AsyncSessionLocal() as session: # безопасно создаём сессию + code = UUID_code_generator() + async with AsyncSessionLocal() as session: async with session.begin(): session.add(SessionCode( code=code,