Files
remnawave-bedolaga-telegram…/app/handlers/balance/wata.py
T
Fringg eb18994b7d fix: complete datetime.utcnow() → datetime.now(UTC) migration
- Migrate 660+ datetime.utcnow() across 153 files to datetime.now(UTC)
- Migrate 30+ datetime.now() without UTC to datetime.now(UTC)
- Convert all 170 DateTime columns to DateTime(timezone=True)
- Add migrate_datetime_to_timestamptz() in universal_migration with SET LOCAL timezone='UTC' safety
- Remove 70+ .replace(tzinfo=None) workarounds
- Fix utcfromtimestamp → fromtimestamp(..., tz=UTC)
- Fix fromtimestamp() without tz= (system_logs, backup_service, referral_diagnostics)
- Fix fromisoformat/isoparse to ensure aware output (platega, yookassa, wata, miniapp, nalogo)
- Fix strptime() to add .replace(tzinfo=UTC) (backup_service, referral_diagnostics)
- Fix datetime.combine() to include tzinfo=UTC (remnawave_sync, traffic_monitoring)
- Fix datetime.max/datetime.min sentinels with .replace(tzinfo=UTC)
- Rename panel_datetime_to_naive_utc → panel_datetime_to_utc
- Remove DTZ003 from ruff ignore list
2026-02-17 04:45:40 +03:00

315 lines
12 KiB
Python

from datetime import UTC, datetime
import structlog
from aiogram import types
from aiogram.fsm.context import FSMContext
from sqlalchemy import update
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database.models import User
from app.keyboards.inline import get_back_keyboard
from app.localization.texts import get_texts
from app.services.payment_service import PaymentService, get_user_by_id as fetch_user_by_id
from app.states import BalanceStates
from app.utils.decorators import error_handler
logger = structlog.get_logger(__name__)
@error_handler
async def start_wata_payment(
callback: types.CallbackQuery,
db_user: User,
state: FSMContext,
):
texts = get_texts(db_user.language)
# Проверка ограничения на пополнение
if getattr(db_user, 'restriction_topup', False):
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
support_url = settings.get_support_contact_url()
keyboard = []
if support_url:
keyboard.append([types.InlineKeyboardButton(text='🆘 Обжаловать', url=support_url)])
keyboard.append([types.InlineKeyboardButton(text=texts.BACK, callback_data='menu_balance')])
await callback.message.edit_text(
f'🚫 <b>Пополнение ограничено</b>\n\n{reason}\n\n'
'Если вы считаете это ошибкой, вы можете обжаловать решение.',
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard),
)
await callback.answer()
return
if not settings.is_wata_enabled():
await callback.answer('❌ Оплата через WATA временно недоступна', show_alert=True)
return
message_text = texts.t(
'WATA_TOPUP_PROMPT',
(
'💳 <b>Оплата через WATA</b>\n\n'
'Введите сумму пополнения. Минимальная сумма — {min_amount}, максимальная — {max_amount}.\n'
'Оплата происходит через защищенную форму WATA.'
),
).format(
min_amount=settings.format_price(settings.WATA_MIN_AMOUNT_KOPEKS),
max_amount=settings.format_price(settings.WATA_MAX_AMOUNT_KOPEKS),
)
keyboard = get_back_keyboard(db_user.language)
if settings.is_quick_amount_buttons_enabled():
from .main import get_quick_amount_buttons
quick_amount_buttons = await get_quick_amount_buttons(db_user.language, db_user)
if quick_amount_buttons:
keyboard.inline_keyboard = quick_amount_buttons + keyboard.inline_keyboard
await callback.message.edit_text(
message_text,
reply_markup=keyboard,
parse_mode='HTML',
)
await state.set_state(BalanceStates.waiting_for_amount)
await state.update_data(
payment_method='wata',
wata_prompt_message_id=callback.message.message_id,
wata_prompt_chat_id=callback.message.chat.id,
)
await callback.answer()
@error_handler
async def process_wata_payment_amount(
message: types.Message,
db_user: User,
db: AsyncSession,
amount_kopeks: int,
state: FSMContext,
):
texts = get_texts(db_user.language)
# Проверка ограничения на пополнение
if getattr(db_user, 'restriction_topup', False):
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
support_url = settings.get_support_contact_url()
keyboard = []
if support_url:
keyboard.append([types.InlineKeyboardButton(text='🆘 Обжаловать', url=support_url)])
keyboard.append([types.InlineKeyboardButton(text=texts.BACK, callback_data='menu_balance')])
await message.answer(
f'🚫 <b>Пополнение ограничено</b>\n\n{reason}\n\n'
'Если вы считаете это ошибкой, вы можете обжаловать решение.',
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=keyboard),
parse_mode='HTML',
)
await state.clear()
return
if not settings.is_wata_enabled():
await message.answer('❌ Оплата через WATA временно недоступна')
return
if amount_kopeks < settings.WATA_MIN_AMOUNT_KOPEKS:
await message.answer(
texts.t(
'WATA_AMOUNT_TOO_LOW',
'Минимальная сумма пополнения: {amount}',
).format(amount=settings.format_price(settings.WATA_MIN_AMOUNT_KOPEKS))
)
return
if amount_kopeks > settings.WATA_MAX_AMOUNT_KOPEKS:
await message.answer(
texts.t(
'WATA_AMOUNT_TOO_HIGH',
'Максимальная сумма пополнения: {amount}',
).format(amount=settings.format_price(settings.WATA_MAX_AMOUNT_KOPEKS))
)
return
payment_service = PaymentService(message.bot)
try:
result = await payment_service.create_wata_payment(
db=db,
user_id=db_user.id,
amount_kopeks=amount_kopeks,
description=settings.get_balance_payment_description(amount_kopeks, telegram_user_id=db_user.telegram_id),
language=db_user.language,
)
except Exception as error: # pragma: no cover - handled by decorator logs
logger.exception('Ошибка создания WATA платежа', error=error)
result = None
if not result or not result.get('payment_url'):
await message.answer(
texts.t(
'WATA_PAYMENT_ERROR',
'❌ Ошибка создания платежа WATA. Попробуйте позже или обратитесь в поддержку.',
)
)
await state.clear()
return
payment_url = result['payment_url']
payment_link_id = result['payment_link_id']
local_payment_id = result['local_payment_id']
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(
text=texts.t('WATA_PAY_BUTTON', '💳 Оплатить через WATA'),
url=payment_url,
)
],
[
types.InlineKeyboardButton(
text=texts.t('CHECK_STATUS_BUTTON', '📊 Проверить статус'),
callback_data=f'check_wata_{local_payment_id}',
)
],
[types.InlineKeyboardButton(text=texts.BACK, callback_data='balance_topup')],
]
)
message_template = texts.t(
'WATA_PAYMENT_INSTRUCTIONS',
(
'💳 <b>Оплата через WATA</b>\n\n'
'💰 Сумма: {amount}\n'
'🆔 ID платежа: {payment_id}\n\n'
'📱 <b>Инструкция:</b>\n'
"1. Нажмите кнопку 'Оплатить через WATA'\n"
'2. Следуйте подсказкам платежной системы\n'
'3. Подтвердите перевод\n'
'4. Средства зачислятся автоматически\n\n'
'❓ Если возникнут проблемы, обратитесь в {support}'
),
)
message_text = message_template.format(
amount=settings.format_price(amount_kopeks),
payment_id=payment_link_id,
support=settings.get_support_contact_display_html(),
)
state_data = await state.get_data()
prompt_message_id = state_data.get('wata_prompt_message_id')
prompt_chat_id = state_data.get('wata_prompt_chat_id', message.chat.id)
try:
await message.delete()
except Exception as delete_error: # pragma: no cover - depends on bot rights
logger.warning('Не удалось удалить сообщение с суммой WATA', delete_error=delete_error)
if prompt_message_id:
try:
await message.bot.delete_message(prompt_chat_id, prompt_message_id)
except Exception as delete_error: # pragma: no cover - diagnostic
logger.warning('Не удалось удалить сообщение с запросом суммы WATA', delete_error=delete_error)
invoice_message = await message.answer(
message_text,
reply_markup=keyboard,
parse_mode='HTML',
)
try:
from app.services import payment_service as payment_module
payment = await payment_module.get_wata_payment_by_local_id(db, local_payment_id)
if payment:
metadata = dict(getattr(payment, 'metadata_json', {}) or {})
metadata['invoice_message'] = {
'chat_id': invoice_message.chat.id,
'message_id': invoice_message.message_id,
}
await db.execute(
update(payment.__class__)
.where(payment.__class__.id == payment.id)
.values(metadata_json=metadata, updated_at=datetime.now(UTC))
)
await db.commit()
except Exception as error: # pragma: no cover - diagnostics
logger.warning('Не удалось сохранить сообщение WATA', error=error)
await state.update_data(
wata_invoice_message_id=invoice_message.message_id,
wata_invoice_chat_id=invoice_message.chat.id,
)
await state.clear()
logger.info(
'Создан WATA платеж для пользователя : ₽, ссылка',
telegram_id=db_user.telegram_id,
amount_kopeks=amount_kopeks / 100,
payment_link_id=payment_link_id,
)
@error_handler
async def check_wata_payment_status(
callback: types.CallbackQuery,
db: AsyncSession,
):
try:
local_payment_id = int(callback.data.split('_')[-1])
except (ValueError, IndexError):
await callback.answer('❌ Некорректный идентификатор платежа', show_alert=True)
return
payment_service = PaymentService(callback.bot)
status_info = await payment_service.get_wata_payment_status(db, local_payment_id)
if not status_info:
await callback.answer('❌ Платеж не найден', show_alert=True)
return
payment = status_info['payment']
user_language = 'ru'
try:
user = await fetch_user_by_id(db, payment.user_id)
if user and getattr(user, 'language', None):
user_language = user.language
except Exception as error:
logger.debug('Не удалось получить пользователя для WATA статуса', error=error)
texts = get_texts(user_language)
status_labels: dict[str, dict[str, str]] = {
'Opened': {'emoji': '', 'label': texts.t('WATA_STATUS_OPENED', 'Ожидает оплаты')},
'Closed': {'emoji': '', 'label': texts.t('WATA_STATUS_CLOSED', 'Обрабатывается')},
'Paid': {'emoji': '', 'label': texts.t('WATA_STATUS_PAID', 'Оплачен')},
'Declined': {'emoji': '', 'label': texts.t('WATA_STATUS_DECLINED', 'Отклонен')},
}
label_info = status_labels.get(
payment.status, {'emoji': '', 'label': texts.t('WATA_STATUS_UNKNOWN', 'Неизвестно')}
)
message_lines = [
texts.t('WATA_STATUS_TITLE', '💳 <b>Статус платежа WATA</b>'),
'',
f'🆔 ID: {payment.payment_link_id}',
f'💰 Сумма: {settings.format_price(payment.amount_kopeks)}',
f'📊 Статус: {label_info["emoji"]} {label_info["label"]}',
f'📅 Создан: {payment.created_at.strftime("%d.%m.%Y %H:%M") if payment.created_at else ""}',
]
if payment.is_paid:
message_lines.append('\n✅ Платеж успешно завершен! Средства уже на балансе.')
elif payment.status in {'Opened', 'Closed'}:
message_lines.append('\n⏳ Платеж еще не завершен. Завершите оплату по ссылке и проверьте статус позже.')
await callback.message.answer('\n'.join(message_lines), parse_mode='HTML')
await callback.answer()