mirror of
https://github.com/olegvodyanov/docbot.git
synced 2025-12-19 23:57:05 +03:00
change tables, update doctor registration logic
This commit is contained in:
parent
2379e6e2a9
commit
414729c41f
1
.gitignore
vendored
1
.gitignore
vendored
@ -8,3 +8,4 @@ db/
|
||||
!src/db
|
||||
__pycache__
|
||||
src/docbot/conversations.pkl
|
||||
conversations.pkl
|
||||
7
src/core/enums/statuses_helpers.py
Normal file
7
src/core/enums/statuses_helpers.py
Normal 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
9
src/core/utils.py
Normal file
@ -0,0 +1,9 @@
|
||||
import uuid
|
||||
|
||||
|
||||
def UUID_code_generator() -> str:
|
||||
"""
|
||||
Генерирует уникальный код в формате UUID.
|
||||
Возвращает строку с кодом.
|
||||
"""
|
||||
return str(uuid.uuid4().hex[:8])
|
||||
@ -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,7 +117,8 @@ 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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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:
|
||||
"""
|
||||
Отдаёт список команд, доступных текущему пользователю:
|
||||
@ -58,3 +59,10 @@ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
|
||||
resize_keyboard=True
|
||||
)
|
||||
await update.message.reply_text(text, parse_mode="Markdown", reply_markup=keyboard)
|
||||
|
||||
|
||||
def get_help_handler() -> CommandHandler:
|
||||
"""
|
||||
Возвращает хэндлер для команды /help.
|
||||
"""
|
||||
return CommandHandler("help", help_command)
|
||||
|
||||
@ -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")
|
||||
|
||||
|
||||
@ -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()
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user