Files
remnawave-bedolaga-telegram…/app/services/payment_service.py
2025-09-07 13:13:03 +03:00

466 lines
20 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.

import logging
import hashlib
import hmac
from typing import Optional, Dict, Any
from datetime import datetime
from aiogram import Bot
from aiogram.types import LabeledPrice
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.services.yookassa_service import YooKassaService
from app.external.telegram_stars import TelegramStarsService
from app.database.crud.yookassa import create_yookassa_payment, link_yookassa_payment_to_transaction
from app.database.crud.transaction import create_transaction
from app.database.crud.user import add_user_balance, get_user_by_id
from app.database.models import TransactionType, PaymentMethod
logger = logging.getLogger(__name__)
class PaymentService:
def __init__(self, bot: Optional[Bot] = None):
self.bot = bot
self.yookassa_service = YooKassaService() if settings.is_yookassa_enabled() else None
self.stars_service = TelegramStarsService(bot) if bot else None
async def create_stars_invoice(
self,
amount_kopeks: int,
description: str,
payload: Optional[str] = None
) -> str:
if not self.bot or not self.stars_service:
raise ValueError("Bot instance required for Stars payments")
try:
amount_rubles = amount_kopeks / 100
stars_amount = TelegramStarsService.calculate_stars_from_rubles(amount_rubles)
invoice_link = await self.bot.create_invoice_link(
title="Пополнение баланса VPN",
description=f"{description} (≈{stars_amount} ⭐)",
payload=payload or f"balance_topup_{amount_kopeks}",
provider_token="",
currency="XTR",
prices=[LabeledPrice(label="Пополнение", amount=stars_amount)]
)
logger.info(f"Создан Stars invoice на {stars_amount} звезд (~{int(amount_rubles)}₽)")
return invoice_link
except Exception as e:
logger.error(f"Ошибка создания Stars invoice: {e}")
raise
async def process_stars_payment(
self,
db: AsyncSession,
user_id: int,
stars_amount: int,
payload: str,
telegram_payment_charge_id: str
) -> bool:
try:
rubles_amount = TelegramStarsService.calculate_rubles_from_stars(stars_amount)
amount_kopeks = int(rubles_amount * 100)
transaction = await create_transaction(
db=db,
user_id=user_id,
type=TransactionType.DEPOSIT,
amount_kopeks=amount_kopeks,
description=f"Пополнение через Telegram Stars ({stars_amount} ⭐)",
payment_method=PaymentMethod.TELEGRAM_STARS,
external_id=telegram_payment_charge_id,
is_completed=True
)
user = await get_user_by_id(db, user_id)
if user:
old_balance = user.balance_kopeks
user.balance_kopeks += amount_kopeks
user.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(user)
logger.info(f"💰 Баланс пользователя {user.telegram_id} изменен: {old_balance}{user.balance_kopeks} (изменение: +{amount_kopeks})")
description_for_referral = f"Пополнение Stars: {int(rubles_amount)}₽ ({stars_amount} ⭐)"
logger.info(f"🔍 Проверка реферальной логики для описания: '{description_for_referral}'")
if any(word in description_for_referral.lower() for word in ["пополнение", "stars", "yookassa", "topup"]) and not any(word in description_for_referral.lower() for word in ["комиссия", "бонус"]):
logger.info(f"🔞 Вызов process_referral_topup для пользователя {user_id}")
try:
from app.services.referral_service import process_referral_topup
await process_referral_topup(db, user_id, amount_kopeks, self.bot)
except Exception as e:
logger.error(f"Ошибка обработки реферального пополнения: {e}")
else:
logger.info(f"❌ Описание '{description_for_referral}' не подходит для реферальной логики")
if self.bot:
try:
from app.services.admin_notification_service import AdminNotificationService
notification_service = AdminNotificationService(self.bot)
await notification_service.send_balance_topup_notification(
db, user, transaction, old_balance
)
except Exception as e:
logger.error(f"Ошибка отправки уведомления о пополнении Stars: {e}")
if self.bot:
try:
await self.bot.send_message(
user.telegram_id,
f"✅ <b>Пополнение успешно!</b>\n\n"
f"⭐ Звезд: {stars_amount}\n"
f"💰 Сумма: {settings.format_price(amount_kopeks)}\n"
f"🦊 Способ: Telegram Stars\n"
f"🆔 Транзакция: {telegram_payment_charge_id[:8]}...\n\n"
f"Баланс пополнен автоматически!",
parse_mode="HTML"
)
logger.info(
f"✅ Отправлено уведомление пользователю {user.telegram_id} о пополнении на {int(rubles_amount)}"
)
except Exception as e:
logger.error(f"Ошибка отправки уведомления о пополнении Stars: {e}")
logger.info(
f"✅ Обработан Stars платеж: пользователь {user_id}, "
f"{stars_amount} звезд → {int(rubles_amount)}"
)
return True
else:
logger.error(f"Пользователь с ID {user_id} не найден при обработке Stars платежа")
return False
except Exception as e:
logger.error(f"Ошибка обработки Stars платежа: {e}", exc_info=True)
return False
async def create_yookassa_payment(
self,
db: AsyncSession,
user_id: int,
amount_kopeks: int,
description: str,
receipt_email: Optional[str] = None,
receipt_phone: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None
) -> Optional[Dict[str, Any]]:
if not self.yookassa_service:
logger.error("YooKassa сервис не инициализирован")
return None
try:
amount_rubles = amount_kopeks / 100
payment_metadata = metadata or {}
payment_metadata.update({
"user_id": str(user_id),
"amount_kopeks": str(amount_kopeks),
"type": "balance_topup"
})
yookassa_response = await self.yookassa_service.create_payment(
amount=amount_rubles,
currency="RUB",
description=description,
metadata=payment_metadata,
receipt_email=receipt_email,
receipt_phone=receipt_phone
)
if not yookassa_response or yookassa_response.get("error"):
logger.error(f"Ошибка создания платежа YooKassa: {yookassa_response}")
return None
yookassa_created_at = None
if yookassa_response.get("created_at"):
try:
dt_with_tz = datetime.fromisoformat(
yookassa_response["created_at"].replace('Z', '+00:00')
)
yookassa_created_at = dt_with_tz.replace(tzinfo=None)
except Exception as e:
logger.warning(f"Не удалось парсить created_at: {e}")
yookassa_created_at = None
local_payment = await create_yookassa_payment(
db=db,
user_id=user_id,
yookassa_payment_id=yookassa_response["id"],
amount_kopeks=amount_kopeks,
currency="RUB",
description=description,
status=yookassa_response["status"],
confirmation_url=yookassa_response.get("confirmation_url"),
metadata_json=payment_metadata,
payment_method_type=None,
yookassa_created_at=yookassa_created_at,
test_mode=yookassa_response.get("test_mode", False)
)
logger.info(f"Создан платеж YooKassa {yookassa_response['id']} на {amount_rubles}₽ для пользователя {user_id}")
return {
"local_payment_id": local_payment.id,
"yookassa_payment_id": yookassa_response["id"],
"confirmation_url": yookassa_response.get("confirmation_url"),
"amount_kopeks": amount_kopeks,
"amount_rubles": amount_rubles,
"status": yookassa_response["status"],
"created_at": local_payment.created_at
}
except Exception as e:
logger.error(f"Ошибка создания платежа YooKassa: {e}")
return None
async def process_yookassa_webhook(self, db: AsyncSession, webhook_data: dict) -> bool:
try:
from app.database.crud.yookassa import (
get_yookassa_payment_by_id,
update_yookassa_payment_status,
link_yookassa_payment_to_transaction
)
from app.database.crud.transaction import create_transaction
from app.database.models import TransactionType, PaymentMethod
payment_object = webhook_data.get("object", {})
yookassa_payment_id = payment_object.get("id")
status = payment_object.get("status")
paid = payment_object.get("paid", False)
if not yookassa_payment_id:
logger.error("Webhook без ID платежа")
return False
payment = await get_yookassa_payment_by_id(db, yookassa_payment_id)
if not payment:
logger.error(f"Платеж не найден в БД: {yookassa_payment_id}")
return False
captured_at = None
if status == "succeeded":
captured_at = datetime.utcnow()
updated_payment = await update_yookassa_payment_status(
db,
yookassa_payment_id,
status,
is_paid=paid,
is_captured=(status == "succeeded"),
captured_at=captured_at,
payment_method_type=payment_object.get("payment_method", {}).get("type")
)
if status == "succeeded" and paid and not updated_payment.transaction_id:
transaction = await create_transaction(
db,
user_id=updated_payment.user_id,
type=TransactionType.DEPOSIT,
amount_kopeks=updated_payment.amount_kopeks,
description=f"Пополнение через YooKassa ({yookassa_payment_id[:8]}...)",
payment_method=PaymentMethod.YOOKASSA,
external_id=yookassa_payment_id,
is_completed=True
)
await link_yookassa_payment_to_transaction(
db, yookassa_payment_id, transaction.id
)
user = await get_user_by_id(db, updated_payment.user_id)
if user:
old_balance = user.balance_kopeks
user.balance_kopeks += updated_payment.amount_kopeks
user.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(user)
try:
from app.services.referral_service import process_referral_topup
await process_referral_topup(db, user.id, updated_payment.amount_kopeks, self.bot)
except Exception as e:
logger.error(f"Ошибка обработки реферального пополнения YooKassa: {e}")
if self.bot:
try:
from app.services.admin_notification_service import AdminNotificationService
notification_service = AdminNotificationService(self.bot)
await notification_service.send_balance_topup_notification(
db, user, transaction, old_balance
)
except Exception as e:
logger.error(f"Ошибка отправки уведомления о пополнении YooKassa: {e}")
if self.bot:
try:
await self.bot.send_message(
user.telegram_id,
f"✅ <b>Пополнение успешно!</b>\n\n"
f"💰 Сумма: {settings.format_price(updated_payment.amount_kopeks)}\n"
f"🦊 Способ: Банковская карта\n"
f"🆔 Транзакция: {yookassa_payment_id[:8]}...\n\n"
f"Баланс пополнен автоматически!",
parse_mode="HTML"
)
logger.info(f"✅ Отправлено уведомление пользователю {user.telegram_id} о пополнении на {updated_payment.amount_kopeks//100}")
except Exception as e:
logger.error(f"Ошибка отправки уведомления о пополнении: {e}")
else:
logger.error(f"Пользователь с ID {updated_payment.user_id} не найден при пополнении баланса")
return False
return True
except Exception as e:
logger.error(f"Ошибка обработки YooKassa webhook: {e}", exc_info=True)
return False
async def _process_successful_yookassa_payment(
self,
db: AsyncSession,
payment: "YooKassaPayment"
) -> bool:
try:
transaction = await create_transaction(
db=db,
user_id=payment.user_id,
transaction_type=TransactionType.DEPOSIT,
amount_kopeks=payment.amount_kopeks,
description=f"Пополнение через YooKassa: {payment.description}",
payment_method=PaymentMethod.YOOKASSA,
external_id=payment.yookassa_payment_id,
is_completed=True
)
await link_yookassa_payment_to_transaction(
db=db,
yookassa_payment_id=payment.yookassa_payment_id,
transaction_id=transaction.id
)
user = await get_user_by_id(db, payment.user_id)
if user:
await add_user_balance(db, user, payment.amount_kopeks, f"Пополнение YooKassa: {payment.amount_kopeks//100}")
logger.info(f"Успешно обработан платеж YooKassa {payment.yookassa_payment_id}: "
f"пользователь {payment.user_id} получил {payment.amount_kopeks/100}")
if self.bot and user:
try:
await self._send_payment_success_notification(
user.telegram_id,
payment.amount_kopeks
)
except Exception as e:
logger.error(f"Ошибка отправки уведомления о платеже: {e}")
return True
except Exception as e:
logger.error(f"Ошибка обработки успешного платежа YooKassa {payment.yookassa_payment_id}: {e}")
return False
async def _send_payment_success_notification(
self,
telegram_id: int,
amount_kopeks: int
) -> None:
if not self.bot:
return
try:
message = (f"✅ <b>Платеж успешно завершен!</b>\n\n"
f"💰 Сумма: {settings.format_price(amount_kopeks)}\n"
f"💳 Способ: Банковская карта (YooKassa)\n\n"
f"Средства зачислены на ваш баланс!")
await self.bot.send_message(
chat_id=telegram_id,
text=message,
parse_mode="HTML"
)
except Exception as e:
logger.error(f"Ошибка отправки уведомления пользователю {telegram_id}: {e}")
async def create_tribute_payment(
self,
amount_kopeks: int,
user_id: int,
description: str
) -> str:
if not settings.TRIBUTE_ENABLED:
raise ValueError("Tribute payments are disabled")
try:
payment_data = {
"amount": amount_kopeks,
"currency": "RUB",
"description": description,
"user_id": user_id,
"callback_url": f"{settings.WEBHOOK_URL}/tribute/callback"
}
payment_url = f"https://tribute.ru/pay?amount={amount_kopeks}&user={user_id}"
logger.info(f"Создан Tribute платеж на {amount_kopeks/100}₽ для пользователя {user_id}")
return payment_url
except Exception as e:
logger.error(f"Ошибка создания Tribute платежа: {e}")
raise
def verify_tribute_webhook(
self,
data: dict,
signature: str
) -> bool:
if not settings.TRIBUTE_API_KEY:
return False
try:
message = str(data).encode()
expected_signature = hmac.new(
settings.TRIBUTE_API_KEY.encode(),
message,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected_signature)
except Exception as e:
logger.error(f"Ошибка проверки Tribute webhook: {e}")
return False
async def process_successful_payment(
self,
payment_id: str,
amount_kopeks: int,
user_id: int,
payment_method: str
) -> bool:
try:
logger.info(f"Обработан успешный платеж: {payment_id}, {amount_kopeks/100}₽, {user_id}")
return True
except Exception as e:
logger.error(f"Ошибка обработки платежа: {e}")
return False