diff --git a/src/core/logging.py b/src/core/logging.py index 7304dae..ffc4b05 100644 --- a/src/core/logging.py +++ b/src/core/logging.py @@ -1,15 +1,38 @@ import logging import sys from logging.handlers import RotatingFileHandler +from typing import Sequence from core.config import settings +class ExtendedLogger(logging.Logger): + """Расширенный логгер с дополнительными методами для форматирования.""" + + def list(self, message: str, items: Sequence, limit: int | None = 20) -> None: + """ + Логирует сообщение и список элементов в читаемом виде. + Пример: + logger.list("Common codes", ["A1", "B2", "C3"]) + """ + if not items: + formatted = "(empty)" + else: + items = [str(i) for i in items] + if limit and len(items) > limit: + formatted = ", ".join(items[:limit]) + f" ... (+{len(items) - limit} more)" + else: + formatted = ", ".join(items) + self.info(f"{message}: {formatted}") def setup_logging(): """ Инициализирует логирование для всего приложения. Конфигурирует консольный и файловый логгеры с ротацией. """ + + # Используем наш кастомный класс логгера + logging.setLoggerClass(ExtendedLogger) + # Получаем уровень логирования из настроек, по умолчанию INFO level_name = settings.LOGGING_LEVEL if hasattr(settings, 'LOGGING_LEVEL') else 'INFO' log_level = getattr(logging, level_name, logging.INFO) diff --git a/src/docbot/services/payments_service.py b/src/docbot/services/payments_service.py index 25b6a4a..0094133 100644 --- a/src/docbot/services/payments_service.py +++ b/src/docbot/services/payments_service.py @@ -29,4 +29,37 @@ async def get_not_mapped_payments() -> Sequence[Row[Tuple[PaymentsRegistered]]] select(PaymentsRegistered) ) - return result.all() \ No newline at end of file + return result.all() + + +async def update_payment_and_session(code: str) -> bool: + """ + Updates both PaymentsRegistered and Sessions in a single transaction. + Returns True if both updates succeed, False if any update fails. + Automatically rolls back if any error occurs. + """ + async with AsyncSessionLocal() as session: + async with session.begin(): + try: + # Update PaymentsRegistered + payment_result = await session.execute( + select(PaymentsRegistered).where(PaymentsRegistered.code == code) + ) + payment = payment_result.scalar_one_or_none() + if not payment or payment.mapped is not None: + return False + payment.mapped = True + + # Update Sessions + session_result = await session.execute( + select(Sessions).where(Sessions.code == code) + ) + 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() + + return True + except Exception as e: + logger.error(f"Transaction failed: {str(e)}") + raise # This will trigger rollback diff --git a/src/docbot/services/session_service.py b/src/docbot/services/session_service.py index 5e26c07..3c8f80d 100644 --- a/src/docbot/services/session_service.py +++ b/src/docbot/services/session_service.py @@ -101,7 +101,7 @@ async def get_session_info(code: str) -> dict | None: } -async def get_sessions_awaiting_payments() -> Sequence[Row[Sessions]]: +async def get_sessions_awaiting_payments() -> Sequence[Row[Sessions]] | bool: async with AsyncSessionLocal() as session: async with session.begin(): result = await session.execute( diff --git a/src/docbot/tasks/payments.py b/src/docbot/tasks/payments.py index e1ecd8c..cb874c6 100644 --- a/src/docbot/tasks/payments.py +++ b/src/docbot/tasks/payments.py @@ -1,9 +1,12 @@ from telegram.ext import ( ContextTypes ) -from docbot.services.payments_service import get_not_mapped_payments +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 core.logging import logger +from sqlalchemy import Row +from db.models import PaymentsRegistered, Sessions # Сопоставляем оплаты, которые пришли от продамуса с сессиями, по которым ещё не было оплаты async def map_payments(context: ContextTypes.DEFAULT_TYPE) -> None: @@ -15,3 +18,20 @@ async def map_payments(context: ContextTypes.DEFAULT_TYPE) -> None: for session in sessions_awaiting_payments: logger.info(f"Неоплаченная сессия: {session.Sessions.code}") + + np_codes = [payment.PaymentsRegistered.code for payment in not_mapped_payments] + sa_codes = [session.Sessions.code for session in sessions_awaiting_payments] + + common_codes: List[str] = list(set(np_codes).intersection(sa_codes)) + + if common_codes: + logger.list("Common codes", common_codes, None) + + # Update the database for matching codes (example using raw SQL) + for code in common_codes: + logger.info(f"Updating payment and session for {code}") + success = await update_payment_and_session(code) + if not success: + logger.error(f"Failed to update payment and session for {code}") + else: + logger.info("Совпадения не найдены!")