eb18994b7d
- 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
315 lines
12 KiB
Python
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()
|