mirror of
https://github.com/olegvodyanov/docbot.git
synced 2025-12-19 23:57:05 +03:00
add payment notification
This commit is contained in:
parent
852c924383
commit
d310e73708
@ -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)
|
||||
60
src/docbot/services/notifications_service.py
Normal file
60
src/docbot/services/notifications_service.py
Normal 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] # не даём разрастись
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
@ -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("Совпадения не найдены!")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user