add payment notification

This commit is contained in:
oleg.vodyanov91@gmail.com 2025-10-25 18:32:36 +04:00
parent 852c924383
commit d310e73708
5 changed files with 132 additions and 5 deletions

View File

@ -1,8 +1,9 @@
from __future__ import annotations
from datetime import datetime
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
from sqlalchemy import String, ForeignKey, BigInteger
from sqlalchemy import String, ForeignKey, BigInteger, UniqueConstraint, Text, Enum
from sqlalchemy.dialects.postgresql import UUID, ARRAY
import enum
import uuid
from typing import List, Optional
@ -179,3 +180,21 @@ class PaymentsRegistered(Base):
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
code: Mapped[str] = mapped_column(String(8), unique=True, nullable=False)
mapped: Mapped[bool] = mapped_column(nullable=True)
class NotificationType(enum.Enum):
PAYMENT_RECEIVED = "payment_received"
class SessionNotification(Base):
__tablename__ = "session_notifications"
__table_args__ = (
UniqueConstraint("session_id", "type", name="uq_session_notification_once"),
)
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)
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)
last_error: Mapped[Optional[str]] = mapped_column(Text, nullable=True)

View File

@ -0,0 +1,60 @@
from __future__ import annotations
from datetime import datetime
from sqlalchemy import select
from sqlalchemy.dialects.postgresql import insert as pg_insert
from db.session import AsyncSessionLocal
from db.models import Sessions, Patients, SessionNotification, NotificationType
async def create_payment_received_once(session_code: str) -> tuple[bool, int | None, SessionNotification | None]:
"""
Пытается создать запись уведомления 'payment_received' для сессии.
Возвращает:
(created: bool, chat_id: int|None, notif_obj: SessionNotification|None)
Если уже существует created=False.
chat_id telegram_id пациента, если найден.
"""
async with AsyncSessionLocal() as session:
# найдём сессию + пациента (для patient_id и chat_id)
s_res = await session.execute(
select(Sessions, Patients)
.join(Patients, Patients.id == Sessions.patient_id)
.where(Sessions.code == session_code)
.limit(1)
)
row = s_res.first()
if not row:
return False, None, None
patient_session, patient = row
# upsert по (session_id, type)
stmt = pg_insert(SessionNotification).values(
session_id=patient_session.id,
patient_id=patient.id,
type=NotificationType.PAYMENT_RECEIVED,
created_at=datetime.utcnow(),
).on_conflict_do_nothing(
index_elements=[SessionNotification.session_id, SessionNotification.type]
).returning(SessionNotification)
n_res = await session.execute(stmt)
notif_obj = n_res.scalar_one_or_none()
created = notif_obj is not None
if created:
# сохраним факт создания
await session.commit()
return created, patient.telegram_id, notif_obj
async def mark_notification_sent(notif_id):
async with AsyncSessionLocal() as session:
async with session.begin():
n = await session.get(SessionNotification, notif_id)
if n:
n.sent_at = datetime.utcnow()
async def mark_notification_error(notif_id, err: str):
async with AsyncSessionLocal() as session:
async with session.begin():
n = await session.get(SessionNotification, notif_id)
if n:
n.last_error = (err or "")[:8000] # не даём разрастись

View File

@ -26,7 +26,7 @@ async def save_payment_completed_info_from_prodamus(code: str) -> bool:
async def get_not_mapped_payments() -> Sequence[Row[Tuple[PaymentsRegistered]]] | None:
async with AsyncSessionLocal() as session:
result = await session.execute(
select(PaymentsRegistered)
select(PaymentsRegistered).where(PaymentsRegistered.mapped.is_(None))
)
return result.all()

View File

@ -2,7 +2,7 @@ from __future__ import annotations
from datetime import datetime
from sqlalchemy import select
from sqlalchemy import select, join
from db.session import AsyncSessionLocal
from db.models import (
@ -110,4 +110,17 @@ async def get_sessions_awaiting_payments() -> Sequence[Row[Sessions]] | bool:
sc: Sessions | None = result.scalars()
if not sc:
return False
return result.all()
return result.all()
async def get_patient_telegram_by_session_code(code: str) -> int | None:
"""
Возвращает telegram_id пациента по коду сессии.
"""
async with AsyncSessionLocal() as session:
result = await session.execute(
select(Patients.telegram_id)
.join(Sessions, Sessions.patient_id == Patients.id)
.where(Sessions.code == code)
)
return result.scalar_one_or_none()

View File

@ -3,11 +3,24 @@ 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
from docbot.services.session_service import get_sessions_awaiting_payments, get_patient_telegram_by_session_code
from docbot.services.notifications_service import (
create_payment_received_once,
mark_notification_sent,
mark_notification_error,
)
from core.logging import logger
from sqlalchemy import Row
from db.models import PaymentsRegistered, Sessions
PAYMENT_OK_TEXT = (
"✅ Оплата получена!\n"
"Позже напомним вам о встрече с врачом.\n"
"Если появятся вопросы — просто напишите сюда."
)
# Сопоставляем оплаты, которые пришли от продамуса с сессиями, по которым ещё не было оплаты
async def map_payments(context: ContextTypes.DEFAULT_TYPE) -> None:
not_mapped_payments: Sequence[Row[PaymentsRegistered]] = await get_not_mapped_payments()
@ -33,5 +46,27 @@ async def map_payments(context: ContextTypes.DEFAULT_TYPE) -> None:
success = await update_payment_and_session(code)
if not success:
logger.error(f"Failed to update payment and session for {code}")
continue
# создаём запись уведомления (если уже есть — не шлём повторно)
created, chat_id, notif_obj = await create_payment_received_once(code)
if not created:
logger.info(f"Notification already exists for {code}, skip sending")
continue
if not chat_id:
logger.error(f"chat_id not found for session code {code}")
continue
try:
await context.bot.send_message(
chat_id=chat_id,
text=PAYMENT_OK_TEXT,
disable_web_page_preview=True,
)
await mark_notification_sent(notif_obj.id)
logger.info(f"Payment notification sent to patient for {code}")
except Exception as e:
await mark_notification_error(notif_obj.id, str(e))
logger.exception(f"Failed to send payment message for {code}: {e}")
else:
logger.info("Совпадения не найдены!")