diff --git a/app/cabinet/routes/subscription_modules/renewal.py b/app/cabinet/routes/subscription_modules/renewal.py index 4e7f892b..48819709 100644 --- a/app/cabinet/routes/subscription_modules/renewal.py +++ b/app/cabinet/routes/subscription_modules/renewal.py @@ -57,7 +57,14 @@ async def get_renewal_options( return [] # Determine available periods - if subscription.tariff_id and subscription.tariff and subscription.tariff.period_prices: + # Скрытый/неактивный тариф (например, триальный после промокода) — + # не показываем его периоды, используем стандартные + if ( + subscription.tariff_id + and subscription.tariff + and subscription.tariff.is_active + and subscription.tariff.period_prices + ): periods = sorted(int(k) for k in subscription.tariff.period_prices.keys()) else: periods = settings.get_available_renewal_periods() @@ -128,7 +135,12 @@ async def renew_subscription( detail=f'Cannot renew subscription with status: {_actual_status}', ) - if subscription.tariff_id and subscription.tariff and subscription.tariff.period_prices: + if ( + subscription.tariff_id + and subscription.tariff + and subscription.tariff.is_active + and subscription.tariff.period_prices + ): available_periods = [int(p) for p in subscription.tariff.period_prices.keys()] else: available_periods = settings.get_available_renewal_periods() diff --git a/app/handlers/balance/main.py b/app/handlers/balance/main.py index ba62df5d..12d5bc22 100644 --- a/app/handlers/balance/main.py +++ b/app/handlers/balance/main.py @@ -491,12 +491,16 @@ async def process_topup_amount(message: types.Message, db_user: User, state: FSM amount_rubles = float(amount_text.replace(',', '.')) if amount_rubles < 1: - await message.answer('Минимальная сумма пополнения: 1 ₽', reply_markup=get_back_keyboard(db_user.language)) + await message.answer( + 'Минимальная сумма пополнения: 1 ₽', + reply_markup=get_back_keyboard(db_user.language, callback_data='balance_topup'), + ) return if amount_rubles > 50000: await message.answer( - 'Максимальная сумма пополнения: 50,000 ₽', reply_markup=get_back_keyboard(db_user.language) + 'Максимальная сумма пополнения: 50,000 ₽', + reply_markup=get_back_keyboard(db_user.language, callback_data='balance_topup'), ) return @@ -509,7 +513,7 @@ async def process_topup_amount(message: types.Message, db_user: User, state: FSM min_rubles = settings.YOOKASSA_MIN_AMOUNT_KOPEKS / 100 await message.answer( f'❌ Минимальная сумма для оплаты через YooKassa: {min_rubles:.0f} ₽', - reply_markup=get_back_keyboard(db_user.language), + reply_markup=get_back_keyboard(db_user.language, callback_data='balance_topup'), ) return @@ -517,7 +521,7 @@ async def process_topup_amount(message: types.Message, db_user: User, state: FSM max_rubles = settings.YOOKASSA_MAX_AMOUNT_KOPEKS / 100 await message.answer( f'❌ Максимальная сумма для оплаты через YooKassa: {max_rubles:,.0f} ₽'.replace(',', ' '), - reply_markup=get_back_keyboard(db_user.language), + reply_markup=get_back_keyboard(db_user.language, callback_data='balance_topup'), ) return @@ -593,6 +597,7 @@ async def handle_topup_amount_callback( platega_method_code = int(method[len('platega_m') :]) await state.update_data(payment_method='platega', platega_method=platega_method_code) + await state.set_state(BalanceStates.waiting_for_amount) async with AsyncSessionLocal() as db: await process_platega_payment_amount(callback.message, db_user, db, amount_kopeks, state) elif method == 'platega': @@ -604,6 +609,7 @@ async def handle_topup_amount_callback( method_code = int(data.get('platega_method', 0)) if data else 0 if method_code > 0: + await state.set_state(BalanceStates.waiting_for_amount) async with AsyncSessionLocal() as db: await process_platega_payment_amount(callback.message, db_user, db, amount_kopeks, state) else: @@ -615,9 +621,12 @@ async def handle_topup_amount_callback( await start_tribute_payment(callback, db_user) return # Стандартные методы через роутер - elif not await route_payment_by_method(callback.message, db_user, amount_kopeks, state, method): - await callback.answer('❌ Неизвестный способ оплаты', show_alert=True) - return + else: + await state.update_data(payment_method=method) + await state.set_state(BalanceStates.waiting_for_amount) + if not await route_payment_by_method(callback.message, db_user, amount_kopeks, state, method): + await callback.answer('❌ Неизвестный способ оплаты', show_alert=True) + return await callback.answer() diff --git a/app/handlers/balance/platega.py b/app/handlers/balance/platega.py index 8db3d5e9..c7d66f5c 100644 --- a/app/handlers/balance/platega.py +++ b/app/handlers/balance/platega.py @@ -42,6 +42,7 @@ async def _prompt_amount( # Если сумма уже известна (например, после быстрого выбора), # сразу создаём платеж и сбрасываем временное значение. await state.update_data(platega_pending_amount=None) + await state.set_state(BalanceStates.waiting_for_amount) from app.database.database import AsyncSessionLocal @@ -294,8 +295,9 @@ async def process_platega_payment_amount( 'PLATEGA_AMOUNT_TOO_LOW', 'Минимальная сумма для оплаты через Platega: {amount}', ).format(amount=settings.format_price(settings.PLATEGA_MIN_AMOUNT_KOPEKS)), - reply_markup=get_back_keyboard(db_user.language), + reply_markup=get_back_keyboard(db_user.language, callback_data='balance_topup'), ) + await state.set_state(BalanceStates.waiting_for_amount) return if amount_kopeks > settings.PLATEGA_MAX_AMOUNT_KOPEKS: @@ -304,8 +306,9 @@ async def process_platega_payment_amount( 'PLATEGA_AMOUNT_TOO_HIGH', 'Максимальная сумма для оплаты через Platega: {amount}', ).format(amount=settings.format_price(settings.PLATEGA_MAX_AMOUNT_KOPEKS)), - reply_markup=get_back_keyboard(db_user.language), + reply_markup=get_back_keyboard(db_user.language, callback_data='balance_topup'), ) + await state.set_state(BalanceStates.waiting_for_amount) return try: diff --git a/app/handlers/subscription/tariff_purchase.py b/app/handlers/subscription/tariff_purchase.py index 57219e4f..dbe70bcd 100644 --- a/app/handlers/subscription/tariff_purchase.py +++ b/app/handlers/subscription/tariff_purchase.py @@ -2142,6 +2142,31 @@ async def show_tariff_extend( await callback.answer('Тариф не найден', show_alert=True) return + # Скрытый/неактивный тариф (например, триальный после промокода) — + # показываем список доступных тарифов вместо продления скрытого + if not tariff.is_active: + promo_group_id = getattr(db_user, 'promo_group_id', None) + tariffs = await get_tariffs_for_user(db, promo_group_id) + active_tariffs = [t for t in tariffs if not t.is_daily] + if not active_tariffs: + await callback.answer('Нет доступных тарифов для продления', show_alert=True) + return + + keyboard = [] + for t in active_tariffs: + keyboard.append([InlineKeyboardButton(text=f'📦 {t.name}', callback_data=f'tariff_select:{t.id}')]) + keyboard.append([InlineKeyboardButton(text='◀️ Назад', callback_data='back_to_menu')]) + + await callback.message.edit_text( + '🔄 Выберите тариф для продления\n\n' + 'Для продления подписки необходимо выбрать тариф.\n' + 'Подписка будет обновлена с параметрами выбранного тарифа.', + reply_markup=InlineKeyboardMarkup(inline_keyboard=keyboard), + parse_mode='HTML', + ) + await callback.answer() + return + traffic = format_traffic(tariff.traffic_limit_gb) # Проверяем есть ли у пользователя скидки по периодам diff --git a/app/services/payment/paypear.py b/app/services/payment/paypear.py index ab8595f6..c5b5e3aa 100644 --- a/app/services/payment/paypear.py +++ b/app/services/payment/paypear.py @@ -106,18 +106,30 @@ class PayPearPaymentMixin: ) confirmation = result.get('confirmation', {}) - payment_url = confirmation.get('url') if isinstance(confirmation, dict) else None + payment_url = ( + (confirmation.get('confirmation_url') or confirmation.get('url')) + if isinstance(confirmation, dict) + else None + ) paypear_id = result.get('id') if not payment_url: - logger.error('PayPear API не вернул URL платежа', result=result) + logger.error('PayPear API не вернул confirmation_url', result=result) return None + # PayPear может добавить комиссию к сумме — сохраняем фактическую сумму + # для корректной проверки в webhook (amount включает комиссию) + api_amount = result.get('amount', {}) + if isinstance(api_amount, dict) and api_amount.get('value') is not None: + charged_kopeks = round(float(api_amount['value']) * 100) + metadata['paypear_charged_kopeks'] = charged_kopeks + logger.info( 'PayPear API: создан платеж', order_id=order_id, paypear_id=paypear_id, payment_url=payment_url, + charged_kopeks=metadata.get('paypear_charged_kopeks'), ) # Срок действия — 30 минут по умолчанию @@ -239,6 +251,8 @@ class PayPearPaymentMixin: } # Проверка суммы ДО обновления статуса + # PayPear добавляет комиссию к amount — сравниваем с сохранённой суммой + # (paypear_charged_kopeks), а не с исходной суммой пополнения if is_paid: amount_info = obj.get('amount', {}) if isinstance(amount_info, dict): @@ -248,11 +262,14 @@ class PayPearPaymentMixin: if amount_value is not None: received_kopeks = round(float(amount_value) * 100) - if abs(received_kopeks - payment.amount_kopeks) > 1: + payment_metadata = dict(getattr(payment, 'metadata_json', {}) or {}) + expected_kopeks = payment_metadata.get('paypear_charged_kopeks', payment.amount_kopeks) + if abs(received_kopeks - expected_kopeks) > 1: logger.error( 'PayPear amount mismatch', - expected_kopeks=payment.amount_kopeks, + expected_kopeks=expected_kopeks, received_kopeks=received_kopeks, + original_amount_kopeks=payment.amount_kopeks, order_id=payment.order_id, ) await paypear_crud.update_paypear_payment_status( @@ -539,16 +556,20 @@ class PayPearPaymentMixin: internal_status, is_paid = status_info if is_paid: - # Проверка суммы + # Проверка суммы — сравниваем с paypear_charged_kopeks + # (amount включает комиссию PayPear) amount_info = order_data.get('amount', {}) api_amount = amount_info.get('value') if isinstance(amount_info, dict) else amount_info if api_amount is not None: received_kopeks = round(float(api_amount) * 100) - if abs(received_kopeks - payment.amount_kopeks) > 1: + payment_metadata = dict(getattr(payment, 'metadata_json', {}) or {}) + expected_kopeks = payment_metadata.get('paypear_charged_kopeks', payment.amount_kopeks) + if abs(received_kopeks - expected_kopeks) > 1: logger.error( 'PayPear amount mismatch (API check)', - expected_kopeks=payment.amount_kopeks, + expected_kopeks=expected_kopeks, received_kopeks=received_kopeks, + original_amount_kopeks=payment.amount_kopeks, order_id=payment.order_id, ) await paypear_crud.update_paypear_payment_status(