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

3
.gitignore vendored
View File

@ -7,4 +7,5 @@ alembic/
db/
!src/db
__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 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"<Doctors(telegram_id={self.telegram_id!r})>"
@ -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"<ReferralCode(code={self.code!r}, is_used={self.is_used})>"
@ -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"<VerificationRequests(code={self.code!r})>"
@ -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")
doctor: Mapped["Doctors"] = relationship(back_populates="payment_methods")

View File

@ -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,

View File

@ -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)
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.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")

View File

@ -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()
return result.scalar_one_or_none()

View File

@ -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

View File

@ -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

View File

@ -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,