Files
remnawave-bedolaga-telegram…/app/services/payment/common.py

212 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Общие инструменты платёжного сервиса.
В этом модуле собраны методы, которые нужны всем платёжным каналам:
построение клавиатур, базовые уведомления и стандартная обработка
успешных платежей.
"""
from __future__ import annotations
import logging
from types import SimpleNamespace
from typing import Any
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from sqlalchemy.exc import MissingGreenlet
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database.crud.user import get_user_by_telegram_id
from app.database.database import get_db
from app.localization.texts import get_texts
from app.services.subscription_checkout_service import (
has_subscription_checkout_draft,
should_offer_checkout_resume,
)
from app.utils.miniapp_buttons import build_miniapp_or_callback_button
logger = logging.getLogger(__name__)
class PaymentCommonMixin:
"""Mixin с базовой логикой, которую используют остальные платёжные блоки."""
async def build_topup_success_keyboard(self, user: Any) -> InlineKeyboardMarkup:
"""Формирует клавиатуру по завершении платежа, подстраиваясь под пользователя."""
# Загружаем нужные тексты с учётом выбранного языка пользователя.
texts = get_texts(user.language if user else "ru")
# Определяем статус подписки, чтобы показать подходящую кнопку.
has_active_subscription = bool(
user and user.subscription and not user.subscription.is_trial and user.subscription.is_active
)
first_button = build_miniapp_or_callback_button(
text=(
texts.MENU_EXTEND_SUBSCRIPTION
if has_active_subscription
else texts.MENU_BUY_SUBSCRIPTION
),
callback_data=(
"subscription_extend" if has_active_subscription else "menu_buy"
),
)
keyboard_rows: list[list[InlineKeyboardButton]] = [[first_button]]
# Если для пользователя есть незавершённый checkout, предлагаем вернуться к нему.
if user:
draft_exists = await has_subscription_checkout_draft(user.id)
if should_offer_checkout_resume(user, draft_exists):
keyboard_rows.append([
build_miniapp_or_callback_button(
text=texts.RETURN_TO_SUBSCRIPTION_CHECKOUT,
callback_data="subscription_resume_checkout",
)
])
# Стандартные кнопки быстрого доступа к балансу и главному меню.
keyboard_rows.append([
build_miniapp_or_callback_button(
text="💰 Мой баланс",
callback_data="menu_balance",
)
])
keyboard_rows.append([
InlineKeyboardButton(
text="🏠 Главное меню",
callback_data="back_to_menu",
)
])
return InlineKeyboardMarkup(inline_keyboard=keyboard_rows)
async def _send_payment_success_notification(
self,
telegram_id: int,
amount_kopeks: int,
user: Any | None = None,
*,
db: AsyncSession | None = None,
payment_method_title: str | None = None,
) -> None:
"""Отправляет пользователю уведомление об успешном платеже."""
if not getattr(self, "bot", None):
# Если бот не передан (например, внутри фоновых задач), уведомление пропускаем.
return
user_snapshot = await self._ensure_user_snapshot(
telegram_id,
user,
db=db,
)
try:
keyboard = await self.build_topup_success_keyboard(user_snapshot)
payment_method = payment_method_title or "Банковская карта (YooKassa)"
message = (
"✅ <b>Платеж успешно завершен!</b>\n\n"
f"💰 Сумма: {settings.format_price(amount_kopeks)}\n"
f"💳 Способ: {payment_method}\n\n"
"Средства зачислены на ваш баланс!"
)
await self.bot.send_message(
chat_id=telegram_id,
text=message,
parse_mode="HTML",
reply_markup=keyboard,
)
except Exception as error:
logger.error(
"Ошибка отправки уведомления пользователю %s: %s",
telegram_id,
error,
)
async def _ensure_user_snapshot(
self,
telegram_id: int,
user: Any | None,
*,
db: AsyncSession | None = None,
) -> Any | None:
"""Гарантирует, что данные пользователя пригодны для построения клавиатуры."""
def _build_snapshot(source: Any | None) -> SimpleNamespace | None:
if source is None:
return None
subscription = getattr(source, "subscription", None)
subscription_snapshot = None
if subscription is not None:
subscription_snapshot = SimpleNamespace(
is_trial=getattr(subscription, "is_trial", False),
is_active=getattr(subscription, "is_active", False),
actual_status=getattr(subscription, "actual_status", None),
)
return SimpleNamespace(
id=getattr(source, "id", None),
telegram_id=getattr(source, "telegram_id", None),
language=getattr(source, "language", "ru"),
subscription=subscription_snapshot,
)
try:
snapshot = _build_snapshot(user)
except MissingGreenlet:
snapshot = None
if snapshot is not None:
return snapshot
fetch_session = db
if fetch_session is not None:
try:
fetched_user = await get_user_by_telegram_id(fetch_session, telegram_id)
return _build_snapshot(fetched_user)
except Exception as fetch_error:
logger.warning(
"Не удалось обновить пользователя %s из переданной сессии: %s",
telegram_id,
fetch_error,
)
try:
async for db_session in get_db():
fetched_user = await get_user_by_telegram_id(db_session, telegram_id)
return _build_snapshot(fetched_user)
except Exception as fetch_error:
logger.warning(
"Не удалось получить пользователя %s для уведомления: %s",
telegram_id,
fetch_error,
)
return None
async def process_successful_payment(
self,
payment_id: str,
amount_kopeks: int,
user_id: int,
payment_method: str,
) -> bool:
"""Общая точка учёта успешных платежей (используется провайдерами при необходимости)."""
try:
logger.info(
"Обработан успешный платеж: %s, %s₽, пользователь %s, метод %s",
payment_id,
amount_kopeks / 100,
user_id,
payment_method,
)
return True
except Exception as error:
logger.error("Ошибка обработки платежа %s: %s", payment_id, error)
return False