change datetime fields, add upcomming sessions check

This commit is contained in:
Oleg Oleg 2025-11-09 02:15:52 +08:00
parent d310e73708
commit e20f789497
10 changed files with 108 additions and 60 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 ещё не заполнен.

View File

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

View File

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