change tables, update doctor registration logic

This commit is contained in:
oleg.vodyanov91@gmail.com 2025-06-08 15:33:19 +03:00
parent 2379e6e2a9
commit 414729c41f
11 changed files with 135 additions and 68 deletions

1
.gitignore vendored
View File

@ -8,3 +8,4 @@ db/
!src/db !src/db
__pycache__ __pycache__
src/docbot/conversations.pkl src/docbot/conversations.pkl
conversations.pkl

View File

@ -0,0 +1,7 @@
from enum import Enum
class ObjectStatuses(str, Enum):
SENT = "Sent"
DECLINED = "Declined"
ACCEPTED = "Accepted"

9
src/core/utils.py Normal file
View File

@ -0,0 +1,9 @@
import uuid
def UUID_code_generator() -> str:
"""
Генерирует уникальный код в формате UUID.
Возвращает строку с кодом.
"""
return str(uuid.uuid4().hex[:8])

View File

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime from datetime import datetime
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship 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 from sqlalchemy.dialects.postgresql import UUID, ARRAY
import uuid import uuid
from typing import List, Optional from typing import List, Optional
@ -14,41 +14,54 @@ class Base(DeclarativeBase):
class SessionCode(Base): class SessionCode(Base):
__tablename__ = "session_codes" __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) code: Mapped[str] = mapped_column(String(8), unique=True, nullable=False)
patient_telegram_id: Mapped[int] = mapped_column(nullable=False) patient_telegram_id: Mapped[int] = mapped_column(nullable=False)
sent_at: Mapped[datetime] = mapped_column(nullable=False) sent_at: Mapped[datetime] = mapped_column(nullable=False)
consulted_at: Mapped[Optional[datetime]] = mapped_column(nullable=True) 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): class Admins(Base):
__tablename__ = "admins" __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) telegram_id: Mapped[int] = mapped_column(unique=True, nullable=False)
created_at: Mapped[Optional[datetime]] = mapped_column(nullable=True) 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): class Doctors(Base):
__tablename__ = "doctors" __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) 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) 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_active: Mapped[bool] = mapped_column(default=False, nullable=False)
is_verified: 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) created_at: Mapped[datetime] = mapped_column(nullable=False)
referral_code_id: Mapped[int] = mapped_column(ForeignKey("referral_codes.id"), nullable=False) referral_code_id: Mapped[int] = mapped_column(
specialties: Mapped[List[str]] = mapped_column(ARRAY(String), nullable=False, default=list) 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() session_codes: Mapped[List["SessionCode"]] = relationship()
form_links: Mapped[List["FormLink"]] = relationship(back_populates="doctor", cascade="all, delete-orphan") form_links: Mapped[List["FormLink"]] = relationship(
payment_methods: Mapped[List["PaymentMethod"]] = relationship(back_populates="doctor", cascade="all, delete-orphan") 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: def __repr__(self) -> str:
return f"<Doctors(telegram_id={self.telegram_id!r})>" return f"<Doctors(telegram_id={self.telegram_id!r})>"
@ -62,7 +75,8 @@ class ReferralCode(Base):
is_used: Mapped[bool] = mapped_column(default=False, nullable=False) is_used: Mapped[bool] = mapped_column(default=False, nullable=False)
created_at: Mapped[datetime] = mapped_column(nullable=False) created_at: Mapped[datetime] = mapped_column(nullable=False)
used_at: Mapped[Optional[datetime]] = mapped_column(nullable=True) 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: def __repr__(self) -> str:
return f"<ReferralCode(code={self.code!r}, is_used={self.is_used})>" return f"<ReferralCode(code={self.code!r}, is_used={self.is_used})>"
@ -72,12 +86,14 @@ class VerificationRequests(Base):
__tablename__ = "verification_requests" __tablename__ = "verification_requests"
id: Mapped[int] = mapped_column(primary_key=True) 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) code: Mapped[str] = mapped_column(unique=True, nullable=False)
sent_at: Mapped[datetime] = mapped_column(nullable=False) sent_at: Mapped[datetime] = mapped_column(nullable=False)
reviewed_at: Mapped[Optional[datetime]] = mapped_column(nullable=True) reviewed_at: Mapped[Optional[datetime]] = mapped_column(nullable=True)
status: Mapped[str] = mapped_column(default=False, nullable=False)
# Связь поправлена — связь с doctor через doctor_id # Связь поправлена — связь с doctor через doctor_id
doctor: Mapped["Doctors"] = relationship() doctor: Mapped["Doctors"] = relationship(back_populates="verification_requests")
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<VerificationRequests(code={self.code!r})>" return f"<VerificationRequests(code={self.code!r})>"
@ -87,7 +103,8 @@ class FormLink(Base):
__tablename__ = "form_links" __tablename__ = "form_links"
id: Mapped[int] = mapped_column(primary_key=True) 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) url: Mapped[str] = mapped_column(nullable=False)
label: Mapped[Optional[str]] = mapped_column(nullable=True) label: Mapped[Optional[str]] = mapped_column(nullable=True)
is_active: Mapped[bool] = mapped_column(default=True, nullable=False) is_active: Mapped[bool] = mapped_column(default=True, nullable=False)
@ -100,7 +117,8 @@ class PaymentMethod(Base):
__tablename__ = "payment_methods" __tablename__ = "payment_methods"
id: Mapped[int] = mapped_column(primary_key=True) 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) method: Mapped[str] = mapped_column(nullable=False)
details: Mapped[Optional[str]] = mapped_column(nullable=True) details: Mapped[Optional[str]] = mapped_column(nullable=True)
is_active: Mapped[bool] = mapped_column(default=True, nullable=False) is_active: Mapped[bool] = mapped_column(default=True, nullable=False)

View File

@ -8,8 +8,10 @@ from telegram.ext import (
) )
from docbot.handlers.utils.cancel_handler import get_cancel_handler 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.enums.dialog_helpers import ConfirmationMessage, Acknowledgement
from core.utils import UUID_code_generator
ASK_REFERRAL_CODE = 1 ASK_REFERRAL_CODE = 1
SEND_ACKNOWLEDGEMENT_INFO = 2 SEND_ACKNOWLEDGEMENT_INFO = 2
@ -32,7 +34,7 @@ SEND_ME_YOUR_SPECIALITY_TEXT = (
"📝 Пожалуйста, введите вашу специальность, в соответствии с которой планируете проводить консультации." "📝 Пожалуйста, введите вашу специальность, в соответствии с которой планируете проводить консультации."
) )
WAIT_FOR_ACTIVATION_TEXT = ( WAIT_FOR_ACTIVATION_TEXT = (
"📝 Заявка принята, направьте диплом и аккредитацию с указанием кода в теме письма на адрес электронной почты:____\n" "📝 Заявка принята, направьте диплом и аккредитацию с указанием кода верификации {0} в теме письма на адрес электронной почты: docbot@docbot.ru\n"
"В этом шаге нужно встроить подсказку: в каком формате направлять диплом, как скачать с госуслуг\n" "В этом шаге нужно встроить подсказку: в каком формате направлять диплом, как скачать с госуслуг\n"
"выписку об аккредитации. Верификация займет 24 часа." "выписку об аккредитации. Верификация займет 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: async def receive_doctor_info_confirmation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
doctor_info_confirmation = update.message.text.strip() doctor_info_confirmation = update.message.text.strip()
if doctor_info_confirmation == ConfirmationMessage.DECLINE.value: if doctor_info_confirmation != ConfirmationMessage.PROCEED.value:
return ConversationHandler.END return ConversationHandler.END
await update.message.reply_text( 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( await update.message.reply_text(
WAIT_FOR_ACTIVATION_TEXT, WAIT_FOR_ACTIVATION_TEXT.format(code),
parse_mode="Markdown", parse_mode="Markdown",
reply_markup=ReplyKeyboardMarkup( reply_markup=ReplyKeyboardMarkup(
reply_keyboard, one_time_keyboard=True, input_field_placeholder="Отправка пакета документов" 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" parse_mode="Markdown"
) )
await mark_referral_code_as_used( await create_doctor(
context.user_data["ref_obj"], context.user_data["ref_obj"],
context.user_data["doctor_telegram_id"], 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 return ConversationHandler.END
@ -178,19 +184,24 @@ def get_register_doctor_first_stage_handler() -> ConversationHandler:
entry_points=[ask_doctor_info_handler()], entry_points=[ask_doctor_info_handler()],
states={ states={
ASK_REFERRAL_CODE: [ ASK_REFERRAL_CODE: [
MessageHandler(filters.TEXT & ~filters.COMMAND, receive_doctor_referral_code) MessageHandler(filters.TEXT & ~filters.COMMAND,
receive_doctor_referral_code)
], ],
SEND_ACKNOWLEDGEMENT_INFO: [ SEND_ACKNOWLEDGEMENT_INFO: [
MessageHandler(filters.TEXT & ~filters.COMMAND, receive_doctor_info_confirmation) MessageHandler(filters.TEXT & ~filters.COMMAND,
receive_doctor_info_confirmation)
], ],
ASK_NAME: [ ASK_NAME: [
MessageHandler(filters.TEXT & ~filters.COMMAND, receive_doctor_name) MessageHandler(filters.TEXT & ~filters.COMMAND,
receive_doctor_name)
], ],
ASK_SPECIALITY: [ ASK_SPECIALITY: [
MessageHandler(filters.TEXT & ~filters.COMMAND, receive_doctor_speciality) MessageHandler(filters.TEXT & ~filters.COMMAND,
receive_doctor_speciality)
], ],
SEND_DIPLOMA_ACK_INFO: [ 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: [ SEND_CONSULTATION_TYPE_ACK_INFO: [
MessageHandler(filters.TEXT & ~filters.COMMAND, MessageHandler(filters.TEXT & ~filters.COMMAND,

View File

@ -1,9 +1,10 @@
from telegram import Update, ReplyKeyboardMarkup 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.admins_service import get_admin_info
from docbot.services.doctors_service import get_doctor_info from docbot.services.doctors_service import get_doctor_info
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
""" """
Отдаёт список команд, доступных текущему пользователю: Отдаёт список команд, доступных текущему пользователю:
@ -58,3 +59,10 @@ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
resize_keyboard=True resize_keyboard=True
) )
await update.message.reply_text(text, parse_mode="Markdown", reply_markup=keyboard) await update.message.reply_text(text, parse_mode="Markdown", reply_markup=keyboard)
def get_help_handler() -> CommandHandler:
"""
Возвращает хэндлер для команды /help.
"""
return CommandHandler("help", help_command)

View File

@ -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.admins.doctors_handler import get_doctors_handler
from docbot.handlers.doctors.register_handler import get_register_doctor_first_stage_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.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.utils.unknown import get_unknown_handler
@ -26,6 +27,7 @@ def main():
app.add_handler(get_doctors_handler()) app.add_handler(get_doctors_handler())
app.add_handler(get_register_doctor_first_stage_handler()) app.add_handler(get_register_doctor_first_stage_handler())
app.add_handler(get_referral_handlers()) app.add_handler(get_referral_handlers())
app.add_handler(get_help_handler())
app.add_handler(get_unknown_handler()) app.add_handler(get_unknown_handler())
logger.debug("Все хэндлеры зарегистрированы, запускаем polling") logger.debug("Все хэндлеры зарегистрированы, запускаем polling")

View File

@ -9,7 +9,7 @@ async def get_admin_info(telegram_id: int) -> Admins | None:
async with AsyncSessionLocal() as session: async with AsyncSessionLocal() as session:
result = await session.execute( result = await session.execute(
select(Admins.telegram_id) select(Admins.telegram_id)
.where(Admins.telegram_id.match(telegram_id)) .where(Admins.telegram_id == telegram_id)
) )
return result.scalar_one_or_none() 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: async with AsyncSessionLocal() as session:
result = await session.execute( result = await session.execute(
select(Admins.telegram_id) select(Admins.telegram_id)
.where(Admins.telegram_id.match(telegram_id)) .where(Admins.telegram_id == telegram_id)
) )
return result.scalar_one_or_none() return result.scalar_one_or_none()

View File

@ -5,15 +5,16 @@ from datetime import datetime
from sqlalchemy import select from sqlalchemy import select
from db.session import AsyncSessionLocal 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.consultation_types import Consultation
from core.enums.statuses_helpers import ObjectStatuses
async def get_doctor_info(telegram_id: int) -> Doctors | None: async def get_doctor_info(telegram_id: int) -> Doctors | None:
async with AsyncSessionLocal() as session: async with AsyncSessionLocal() as session:
result = await session.execute( result = await session.execute(
select(Doctors) select(Doctors)
.where(Doctors.telegram_id.match(telegram_id)) .where(Doctors.telegram_id == telegram_id)
) )
return result.scalar_one_or_none() 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, is_active=is_active,
created_at=datetime.utcnow() 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

View File

@ -15,7 +15,8 @@ async def generate_referral_code(length: int = 12) -> str:
""" """
alphabet = string.ascii_uppercase + string.digits alphabet = string.ascii_uppercase + string.digits
while True: 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: async with AsyncSessionLocal() as session:
result = await session.execute( result = await session.execute(
select(ReferralCode) select(ReferralCode)
@ -23,7 +24,8 @@ async def generate_referral_code(length: int = 12) -> str:
) )
exists = result.scalar_one_or_none() exists = result.scalar_one_or_none()
if not exists: 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) session.add(new_ref)
await session.commit() await session.commit()
return referral_code return referral_code
@ -43,28 +45,3 @@ async def validate_referral_code(referral_code: str) -> ReferralCode | None:
) )
) )
return result.scalar_one_or_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

View File

@ -7,15 +7,16 @@ from sqlalchemy import select
from db.session import AsyncSessionLocal from db.session import AsyncSessionLocal
from db.models import SessionCode from db.models import SessionCode
from core.utils import UUID_code_generator
async def create_session_code(telegram_id: int, form_link: str) -> str: async def create_session_code(telegram_id: int, form_link: str) -> str:
""" """
Генерирует уникальный код, сохраняет его вместе с Telegram ID и ссылкой на анкету. Генерирует уникальный код, сохраняет его вместе с Telegram ID.
Возвращает этот код. Возвращает этот код.
""" """
code = uuid.uuid4().hex[:8] code = UUID_code_generator()
async with AsyncSessionLocal() as session: # безопасно создаём сессию async with AsyncSessionLocal() as session:
async with session.begin(): async with session.begin():
session.add(SessionCode( session.add(SessionCode(
code=code, code=code,