diff --git a/src/db/models.py b/src/db/models.py index 43f5408..2d207eb 100644 --- a/src/db/models.py +++ b/src/db/models.py @@ -1,10 +1,10 @@ from __future__ import annotations from datetime import datetime from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship -from sqlalchemy import String, ForeignKey, BigInteger, UniqueConstraint, Text, Enum -from sqlalchemy.dialects.postgresql import UUID, ARRAY +from sqlalchemy.sql import func +from sqlalchemy import String, ForeignKey, BigInteger, UniqueConstraint, Text, DateTime +from sqlalchemy.dialects.postgresql import ARRAY import enum -import uuid from typing import List, Optional @@ -15,44 +15,39 @@ class Base(DeclarativeBase): class Sessions(Base): __tablename__ = "sessions" - id: Mapped[uuid.UUID] = mapped_column( - UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) code: Mapped[str] = mapped_column(String(8), unique=True, nullable=False) patient_id: Mapped[int] = mapped_column( ForeignKey("patients.id", ondelete="CASCADE"), nullable=False) patient: Mapped["Patients"] = relationship(back_populates="sessions") - sent_at: Mapped[datetime] = mapped_column(nullable=False) - consulted_at: Mapped[Optional[datetime]] = mapped_column(nullable=True) + sent_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + consulted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) doctor_id: Mapped[int] = mapped_column(ForeignKey("doctors.id")) session_status_history: Mapped[List["SessionStatusHistory"]] = relationship( back_populates="sessions", cascade="all, delete-orphan") session_date_time_history: Mapped[List["SessionDateTimeHistory"]] = relationship( back_populates="sessions", cascade="all, delete-orphan") - paid_at: Mapped[Optional[datetime]] = mapped_column(nullable=True) + paid_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) class SessionDateTimeHistory(Base): __tablename__ = "session_date_time_history" - id: Mapped[uuid.UUID] = mapped_column( - UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - sessions_id: Mapped[uuid.UUID] = mapped_column( - UUID(as_uuid=True), ForeignKey("sessions.id", ondelete="CASCADE"), nullable=False) + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + sessions_id: Mapped[int] = mapped_column(ForeignKey("sessions.id", ondelete="CASCADE"), nullable=False) sessions: Mapped["Sessions"] = relationship(back_populates="session_date_time_history") - updated_at: Mapped[datetime] = mapped_column(nullable=False, default=datetime.utcnow) - consultation_date_time: Mapped[Optional[datetime]] = mapped_column(nullable=True) + updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + consultation_date_time: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) who_updated: Mapped[Optional[str]] = mapped_column(String(50), nullable=True) class SessionStatusHistory(Base): __tablename__ = "session_status_history" - id: Mapped[uuid.UUID] = mapped_column( - UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - sessions_id: Mapped[uuid.UUID] = mapped_column( - UUID(as_uuid=True), ForeignKey("sessions.id", ondelete="CASCADE"), nullable=False) + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + sessions_id: Mapped[int] = mapped_column(ForeignKey("sessions.id", ondelete="CASCADE"), nullable=False) sessions: Mapped["Sessions"] = relationship(back_populates="session_status_history") - updated_at: Mapped[datetime] = mapped_column(nullable=False, default=datetime.utcnow) + updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) status: Mapped[str] = mapped_column(String(50), nullable=False, default="pending") who_updated: Mapped[Optional[str]] = mapped_column(String(50), nullable=True) @@ -62,7 +57,7 @@ class Admins(Base): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) telegram_id: Mapped[int] = mapped_column(BigInteger, unique=True, nullable=False) - created_at: Mapped[Optional[datetime]] = mapped_column(nullable=True) + created_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) available_payment_methods: Mapped[Optional[List[str]]] = mapped_column( ARRAY(String), nullable=True) @@ -73,8 +68,8 @@ class Patients(Base): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) telegram_id: Mapped[int] = mapped_column(BigInteger, unique=True, nullable=False) phone: Mapped[Optional[str]] = mapped_column(nullable=True) - created_at: Mapped[datetime] = mapped_column(nullable=False) - updated_at: Mapped[Optional[datetime]] = mapped_column(nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) accepted_terms: Mapped[bool] = mapped_column(default=False, nullable=True) sessions: Mapped[List["Sessions"]] = relationship( back_populates="patient", cascade="all, delete-orphan") @@ -92,12 +87,11 @@ class Doctors(Base): ] = 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) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) 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) sessions: Mapped[List["Sessions"]] = relationship() @@ -118,8 +112,8 @@ class ReferralCode(Base): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) code: Mapped[str] = mapped_column(unique=True, nullable=False) 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) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + used_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) doctor: Mapped[Optional["Doctors"]] = relationship( back_populates="referral", uselist=False) @@ -134,8 +128,8 @@ class VerificationRequests(Base): doctor_id: Mapped[int] = mapped_column( ForeignKey("doctors.id", ondelete="CASCADE"), unique=True, 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) + sent_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + reviewed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) status: Mapped[str] = mapped_column(default=False, nullable=False) # Связь поправлена — связь с doctor через doctor_id doctor: Mapped["Doctors"] = relationship( @@ -154,7 +148,7 @@ class FormLink(Base): 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) - created_at: Mapped[datetime] = mapped_column(nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) doctor: Mapped["Doctors"] = relationship(back_populates="form_links") @@ -170,7 +164,7 @@ class PaymentMethod(Base): payment_api_key: Mapped[str] = mapped_column(nullable=False) is_active: Mapped[bool] = mapped_column(default=True, nullable=False) is_primary: Mapped[bool] = mapped_column(default=True, nullable=False) - created_at: Mapped[datetime] = mapped_column(nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) doctor: Mapped["Doctors"] = relationship(back_populates="payment_methods") @@ -192,9 +186,9 @@ class SessionNotification(Base): ) id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) - session_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("sessions.id", ondelete="CASCADE"), nullable=False) + session_id: Mapped[int] = mapped_column(ForeignKey("sessions.id", ondelete="CASCADE"), nullable=False) patient_id: Mapped[int] = mapped_column(ForeignKey("patients.id", ondelete="CASCADE"), nullable=False) type: Mapped[NotificationType] = mapped_column(nullable=False) - created_at: Mapped[datetime] = mapped_column(nullable=False, default=datetime.utcnow) - sent_at: Mapped[Optional[datetime]] = mapped_column(nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + sent_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) last_error: Mapped[Optional[str]] = mapped_column(Text, nullable=True) \ No newline at end of file diff --git a/src/docbot/main.py b/src/docbot/main.py index e74a110..fac5ccf 100644 --- a/src/docbot/main.py +++ b/src/docbot/main.py @@ -14,6 +14,7 @@ from docbot.handlers.utils.help import get_help_handler from docbot.handlers.utils.unknown import get_unknown_handler from docbot.handlers.utils.cancel_handler import get_cancel_handler from docbot.tasks.payments import map_payments +from docbot.tasks.sessions import get_sessions_with_consultation_datetime def main(): @@ -46,7 +47,13 @@ def main(): logger.info("Запускаем таски") job_queue = app.job_queue - payments_mapping_task = job_queue.run_repeating(map_payments, interval=60, first=10) + payments_mapping_task = job_queue.run_repeating( + map_payments, interval=60, job_kwargs={'misfire_grace_time': 10} + ) # Таска сопоставления оплат сессиям + + upcoming_sessions = job_queue.run_repeating( + get_sessions_with_consultation_datetime, interval=10, job_kwargs={'misfire_grace_time': 10} + ) logger.info("Таски запущены") app.run_polling() diff --git a/src/docbot/services/doctors_service.py b/src/docbot/services/doctors_service.py index ec5e7f1..c1a22a6 100644 --- a/src/docbot/services/doctors_service.py +++ b/src/docbot/services/doctors_service.py @@ -1,6 +1,6 @@ from __future__ import annotations -from datetime import datetime +from datetime import datetime, timezone from sqlalchemy import select @@ -78,7 +78,7 @@ async def add_payment_link(telegram_id: int, payment_link: str, payment_api_key: details="", is_active=True, is_primary=False if await is_there_primary_payment_method() else True, - created_at=datetime.utcnow() + created_at=datetime.now(timezone.utc) )) @@ -90,7 +90,7 @@ async def add_doctor(telegram_id: int, name: str, available_formats: Consultatio name=name, available_formats=available_formats, is_active=is_active, - created_at=datetime.utcnow() + created_at=datetime.now(timezone.utc) )) @@ -103,14 +103,14 @@ async def create_doctor(referral_obj: ReferralCode, telegram_id: int, name: str, async with session.begin(): # помечаем код referral_obj.is_used = True - referral_obj.used_at = datetime.utcnow() + referral_obj.used_at = datetime.now(timezone.utc) # создаём врача new_doc = Doctors( telegram_id=telegram_id, name=name, is_active=False, - created_at=datetime.utcnow(), + created_at=datetime.now(timezone.utc), referral=referral_obj, code=code, time_zone=time_zone @@ -118,10 +118,10 @@ async def create_doctor(referral_obj: ReferralCode, telegram_id: int, name: str, verification_request = VerificationRequests() verification_request.code = verification_code - verification_request.requested_at = datetime.utcnow() + verification_request.requested_at = datetime.now(timezone.utc) verification_request.status = ObjectStatuses.SENT.value verification_request.doctor = new_doc - verification_request.sent_at = datetime.utcnow() + verification_request.sent_at = datetime.now(timezone.utc) session.add(verification_request) session.add(new_doc) await session.commit() diff --git a/src/docbot/services/notifications_service.py b/src/docbot/services/notifications_service.py index 7fa1d2d..c05e827 100644 --- a/src/docbot/services/notifications_service.py +++ b/src/docbot/services/notifications_service.py @@ -1,5 +1,5 @@ from __future__ import annotations -from datetime import datetime +from datetime import datetime, timezone from sqlalchemy import select from sqlalchemy.dialects.postgresql import insert as pg_insert @@ -32,7 +32,7 @@ async def create_payment_received_once(session_code: str) -> tuple[bool, int | N session_id=patient_session.id, patient_id=patient.id, type=NotificationType.PAYMENT_RECEIVED, - created_at=datetime.utcnow(), + created_at=datetime.now(timezone.utc), ).on_conflict_do_nothing( index_elements=[SessionNotification.session_id, SessionNotification.type] ).returning(SessionNotification) @@ -50,7 +50,7 @@ async def mark_notification_sent(notif_id): async with session.begin(): n = await session.get(SessionNotification, notif_id) if n: - n.sent_at = datetime.utcnow() + n.sent_at = datetime.now(timezone.utc) async def mark_notification_error(notif_id, err: str): async with AsyncSessionLocal() as session: diff --git a/src/docbot/services/patients_service.py b/src/docbot/services/patients_service.py index 62861a7..0744427 100644 --- a/src/docbot/services/patients_service.py +++ b/src/docbot/services/patients_service.py @@ -1,9 +1,8 @@ from __future__ import annotations -from datetime import datetime +from datetime import datetime, timezone from sqlalchemy import select from db.session import AsyncSessionLocal from db.models import Patients -from core.logging import logger async def update_patient_phone(telegram_id: int, phone: str) -> bool: @@ -22,7 +21,7 @@ async def update_patient_phone(telegram_id: int, phone: str) -> bool: patient = result.scalar_one_or_none() if patient: patient.phone = phone - patient.updated_at = datetime.utcnow() + patient.updated_at = datetime.now(timezone.utc) return True return False @@ -38,7 +37,7 @@ async def create_patient(telegram_id: int, terms_acceptance: bool) -> Patients: async with session.begin(): new_patient = Patients( telegram_id=telegram_id, - created_at=datetime.utcnow(), + created_at=datetime.now(timezone.utc), accepted_terms=terms_acceptance ) session.add(new_patient) diff --git a/src/docbot/services/payments_service.py b/src/docbot/services/payments_service.py index 5020186..c6eee07 100644 --- a/src/docbot/services/payments_service.py +++ b/src/docbot/services/payments_service.py @@ -1,6 +1,6 @@ from __future__ import annotations -from datetime import datetime +from datetime import datetime, timezone from sqlalchemy import select @@ -57,7 +57,7 @@ async def update_payment_and_session(code: str) -> bool: session_rec = session_result.scalar_one_or_none() if not session_rec or session_rec.paid_at is not None: return False - session_rec.paid_at = datetime.utcnow() + session_rec.paid_at = datetime.now(timezone.utc) return True except Exception as e: diff --git a/src/docbot/services/referral_service.py b/src/docbot/services/referral_service.py index b8a2cfa..2830af9 100644 --- a/src/docbot/services/referral_service.py +++ b/src/docbot/services/referral_service.py @@ -1,6 +1,6 @@ import secrets import string -from datetime import datetime +from datetime import datetime, timezone from sqlalchemy import select @@ -25,7 +25,7 @@ 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()) + code=referral_code, created_at=datetime.now(timezone.utc)) session.add(new_ref) await session.commit() return referral_code diff --git a/src/docbot/services/session_service.py b/src/docbot/services/session_service.py index 500e594..a386af1 100644 --- a/src/docbot/services/session_service.py +++ b/src/docbot/services/session_service.py @@ -1,13 +1,14 @@ from __future__ import annotations -from datetime import datetime +from datetime import datetime, timezone +from typing import Sequence -from sqlalchemy import select, join +from sqlalchemy import Sequence, Row, select from db.session import AsyncSessionLocal from db.models import ( Sessions, Patients, SessionStatusHistory, SessionDateTimeHistory, - PaymentsRegistered + Doctors ) from core.utils import (generate_session_code, date_time_formatter) from core.logging import logger @@ -25,20 +26,20 @@ async def create_session(telegram_id: int, phone: str, consultation_date_time: s sessions = Sessions( code=code, patient=patient, - sent_at=datetime.utcnow(), + sent_at=datetime.now(timezone.utc), doctor_id=doctor_id ) sessions_code_history = SessionStatusHistory( sessions=sessions, - updated_at=datetime.utcnow(), + updated_at=datetime.now(timezone.utc), status="created", who_updated="bot" ) sessions_date_time_history = SessionDateTimeHistory( sessions=sessions, - updated_at=datetime.utcnow(), + updated_at=datetime.now(timezone.utc), consultation_date_time=date_time_formatter(consultation_date_time), who_updated="bot" ) @@ -61,10 +62,43 @@ async def mark_consulted(code: str) -> bool: sc: Sessions | None = result.scalar_one_or_none() if not sc: return False - sc.consulted_at = datetime.utcnow() + sc.consulted_at = datetime.now(timezone.utc) return True +async def get_all_upcomming_sessions() -> Sequence[Row[Sessions]] | bool: + """ + Возвращает все сессии, у которых consulted_at ещё не заполнен. + """ + async with AsyncSessionLocal() as session: + async with session.begin(): + q = ( + select( + SessionDateTimeHistory.consultation_date_time.label("date"), + Sessions.code.label("code"), + Patients.telegram_id.label("telegram_id"), + Doctors.time_zone.label("time_zone") + ) + .join(Sessions, Sessions.id == SessionDateTimeHistory.sessions_id) + .join(Patients, Patients.id == Sessions.patient_id) + .join(Doctors, Doctors.id == Sessions.doctor_id) + .where(Sessions.paid_at.is_not(None), Sessions.consulted_at.is_(None)) + .subquery() + ) + + stmt = ( + select(q.c.telegram_id, q.c.code, q.c.date, q.c.time_zone) + .distinct(q.c.code) + .order_by(q.c.code, q.c.date.desc()) + ) + + result = await session.execute(stmt) + sc = result.scalars() + if not sc: + return False + return result.all() + + async def get_pending_session(telegram_id: int) -> Sessions | None: """ Ищет самую «свежую» сессию по telegram_id, где consulted_at ещё не заполнен. diff --git a/src/docbot/tasks/payments.py b/src/docbot/tasks/payments.py index afdbdd2..b4732c7 100644 --- a/src/docbot/tasks/payments.py +++ b/src/docbot/tasks/payments.py @@ -3,7 +3,7 @@ from telegram.ext import ( ) from typing import Sequence, List from docbot.services.payments_service import get_not_mapped_payments, update_payment_and_session -from docbot.services.session_service import get_sessions_awaiting_payments, get_patient_telegram_by_session_code +from docbot.services.session_service import get_sessions_awaiting_payments from docbot.services.notifications_service import ( create_payment_received_once, mark_notification_sent, @@ -23,6 +23,7 @@ PAYMENT_OK_TEXT = ( # Сопоставляем оплаты, которые пришли от продамуса с сессиями, по которым ещё не было оплаты async def map_payments(context: ContextTypes.DEFAULT_TYPE) -> None: + logger.info(f"Запуск таски сопоставления оплат сессиям") not_mapped_payments: Sequence[Row[PaymentsRegistered]] = await get_not_mapped_payments() sessions_awaiting_payments: Sequence[Row[Sessions]] = await get_sessions_awaiting_payments() diff --git a/src/docbot/tasks/sessions.py b/src/docbot/tasks/sessions.py new file mode 100644 index 0000000..2f64c2c --- /dev/null +++ b/src/docbot/tasks/sessions.py @@ -0,0 +1,13 @@ +from telegram.ext import ( + ContextTypes +) +from docbot.services.session_service import get_all_upcomming_sessions +from core.logging import logger + +async def get_sessions_with_consultation_datetime(context: ContextTypes.DEFAULT_TYPE) -> None: + logger.info("Fetching sessions with consultation datetime") + sessions = await get_all_upcomming_sessions() + if sessions: + logger.info(f"Found {len(sessions)} upcoming sessions:") + for session in sessions: + logger.info(f"Telegram: {session.telegram_id} Session code: {session.code}, datetime: {session.date}, time_zone: {session.time_zone}")