diff --git a/app/config.py b/app/config.py index dc903964..049ca7d2 100644 --- a/app/config.py +++ b/app/config.py @@ -491,8 +491,14 @@ class Settings(BaseSettings): return bool(getattr(self, "LANGUAGE_SELECTION_ENABLED", True)) def format_price(self, price_kopeks: int) -> str: - rubles = price_kopeks // 100 - return f"{rubles} ₽" + sign = "-" if price_kopeks < 0 else "" + rubles, kopeks = divmod(abs(price_kopeks), 100) + + if kopeks: + value = f"{sign}{rubles}.{kopeks:02d}".rstrip("0").rstrip(".") + return f"{value} ₽" + + return f"{sign}{rubles} ₽" def get_reports_chat_id(self) -> Optional[str]: if self.ADMIN_REPORTS_CHAT_ID: diff --git a/app/external/telegram_stars.py b/app/external/telegram_stars.py index 8a0099cd..de81cfbb 100644 --- a/app/external/telegram_stars.py +++ b/app/external/telegram_stars.py @@ -1,4 +1,5 @@ import logging +from decimal import Decimal, ROUND_HALF_UP from typing import Optional, Dict, Any from aiogram import Bot from aiogram.types import LabeledPrice, InlineKeyboardMarkup, InlineKeyboardButton @@ -18,8 +19,9 @@ class TelegramStarsService: return settings.rubles_to_stars(rubles) @staticmethod - def calculate_rubles_from_stars(stars: int) -> float: - return settings.stars_to_rubles(stars) + def calculate_rubles_from_stars(stars: int) -> Decimal: + rate = Decimal(str(settings.get_stars_rate())) + return (Decimal(stars) * rate).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) async def create_invoice( self, @@ -31,10 +33,10 @@ class TelegramStarsService: start_parameter: Optional[str] = None ) -> Optional[str]: try: - amount_rubles = amount_kopeks / 100 - stars_amount = self.calculate_stars_from_rubles(amount_rubles) + amount_rubles = Decimal(amount_kopeks) / Decimal(100) + stars_amount = self.calculate_stars_from_rubles(float(amount_rubles)) stars_rate = settings.get_stars_rate() - + invoice_link = await self.bot.create_invoice_link( title=title, description=description, @@ -46,7 +48,7 @@ class TelegramStarsService: ) logger.info( - f"Создан Stars invoice на {stars_amount} звезд (~{int(amount_rubles)}₽) " + f"Создан Stars invoice на {stars_amount} звезд (~{settings.format_price(amount_kopeks)}) " f"для {chat_id}, курс: {stars_rate}₽/⭐" ) return invoice_link @@ -65,8 +67,8 @@ class TelegramStarsService: keyboard: Optional[InlineKeyboardMarkup] = None ) -> Optional[Dict[str, Any]]: try: - amount_rubles = amount_kopeks / 100 - stars_amount = self.calculate_stars_from_rubles(amount_rubles) + amount_rubles = Decimal(amount_kopeks) / Decimal(100) + stars_amount = self.calculate_stars_from_rubles(float(amount_rubles)) stars_rate = settings.get_stars_rate() message = await self.bot.send_invoice( @@ -82,12 +84,12 @@ class TelegramStarsService: logger.info( f"Отправлен Stars invoice {message.message_id} на {stars_amount} звезд " - f"(~{int(amount_rubles)}₽), курс: {stars_rate}₽/⭐" + f"(~{settings.format_price(amount_kopeks)}), курс: {stars_rate}₽/⭐" ) return { "message_id": message.message_id, "stars_amount": stars_amount, - "rubles_amount": amount_rubles, + "rubles_amount": float(amount_rubles), "payload": payload } diff --git a/app/handlers/stars_payments.py b/app/handlers/stars_payments.py index 5e251c12..254ebd37 100644 --- a/app/handlers/stars_payments.py +++ b/app/handlers/stars_payments.py @@ -1,7 +1,9 @@ import logging +from decimal import Decimal, ROUND_HALF_UP from aiogram import Dispatcher, types, F from sqlalchemy.ext.asyncio import AsyncSession +from app.config import settings from app.database.models import User from app.services.payment_service import PaymentService from app.external.telegram_stars import TelegramStarsService @@ -114,6 +116,8 @@ async def handle_successful_payment( if success: rubles_amount = TelegramStarsService.calculate_rubles_from_stars(payment.total_amount) + amount_kopeks = int((rubles_amount * Decimal(100)).to_integral_value(rounding=ROUND_HALF_UP)) + amount_text = settings.format_price(amount_kopeks).replace(" ₽", "") keyboard = await payment_service.build_topup_success_keyboard(user) @@ -129,7 +133,7 @@ async def handle_successful_payment( "Спасибо за пополнение! 🚀", ).format( stars_spent=payment.total_amount, - amount=int(rubles_amount), + amount=amount_text, transaction_id=transaction_id_short, ), parse_mode="HTML", @@ -137,8 +141,10 @@ async def handle_successful_payment( ) logger.info( - f"✅ Stars платеж успешно обработан: " - f"пользователь {user.id}, {payment.total_amount} звезд → {int(rubles_amount)}₽" + "✅ Stars платеж успешно обработан: пользователь %s, %s звезд → %s", + user.id, + payment.total_amount, + settings.format_price(amount_kopeks), ) else: logger.error(f"Ошибка обработки Stars платежа для пользователя {user.id}") diff --git a/app/services/payment_service.py b/app/services/payment_service.py index 6523375d..623790eb 100644 --- a/app/services/payment_service.py +++ b/app/services/payment_service.py @@ -2,7 +2,7 @@ import logging import hashlib import hmac import uuid -from decimal import Decimal, InvalidOperation +from decimal import Decimal, InvalidOperation, ROUND_HALF_UP from typing import Optional, Dict, Any from datetime import datetime from aiogram import Bot @@ -113,8 +113,8 @@ class PaymentService: raise ValueError("Bot instance required for Stars payments") try: - amount_rubles = amount_kopeks / 100 - stars_amount = TelegramStarsService.calculate_stars_from_rubles(amount_rubles) + amount_rubles = Decimal(amount_kopeks) / Decimal(100) + stars_amount = TelegramStarsService.calculate_stars_from_rubles(float(amount_rubles)) invoice_link = await self.bot.create_invoice_link( title="Пополнение баланса VPN", @@ -125,7 +125,11 @@ class PaymentService: prices=[LabeledPrice(label="Пополнение", amount=stars_amount)] ) - logger.info(f"Создан Stars invoice на {stars_amount} звезд (~{int(amount_rubles)}₽)") + logger.info( + "Создан Stars invoice на %s звезд (~%s)", + stars_amount, + settings.format_price(amount_kopeks), + ) return invoice_link except Exception as e: @@ -142,7 +146,7 @@ class PaymentService: ) -> bool: try: rubles_amount = TelegramStarsService.calculate_rubles_from_stars(stars_amount) - amount_kopeks = int(rubles_amount * 100) + amount_kopeks = int((rubles_amount * Decimal(100)).to_integral_value(rounding=ROUND_HALF_UP)) transaction = await create_transaction( db=db, @@ -167,7 +171,9 @@ class PaymentService: logger.info(f"💰 Баланс пользователя {user.telegram_id} изменен: {old_balance} → {user.balance_kopeks} (изменение: +{amount_kopeks})") - description_for_referral = f"Пополнение Stars: {int(rubles_amount)}₽ ({stars_amount} ⭐)" + description_for_referral = ( + f"Пополнение Stars: {settings.format_price(amount_kopeks)} ({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 ["комиссия", "бонус"]): @@ -206,14 +212,18 @@ class PaymentService: reply_markup=keyboard, ) logger.info( - f"✅ Отправлено уведомление пользователю {user.telegram_id} о пополнении на {int(rubles_amount)}₽" + "✅ Отправлено уведомление пользователю %s о пополнении на %s", + user.telegram_id, + settings.format_price(amount_kopeks), ) except Exception as e: logger.error(f"Ошибка отправки уведомления о пополнении Stars: {e}") logger.info( - f"✅ Обработан Stars платеж: пользователь {user_id}, " - f"{stars_amount} звезд → {int(rubles_amount)}₽" + "✅ Обработан Stars платеж: пользователь %s, %s звезд → %s", + user_id, + stars_amount, + settings.format_price(amount_kopeks), ) return True else: