From 23ff40cd2c24c1410d3565a92c163eb612682ef5 Mon Sep 17 00:00:00 2001 From: firewookie Date: Thu, 12 Mar 2026 09:31:46 +0500 Subject: [PATCH 1/4] =?UTF-8?q?=D0=9E=D1=82=D0=BE=D0=B1=D1=80=D0=B0=D0=B6?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D1=80=D0=B8=D0=B2=D1=8F=D0=B7?= =?UTF-8?q?=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=BA=D0=B0=D1=80=D1=82=20?= =?UTF-8?q?=D0=B2=20=D1=80=D0=B0=D0=B7=D0=B4=D0=B5=D0=BB=D0=B5=20=D0=B2=20?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/handlers/balance/main.py | 6 ++ app/handlers/subscription/__init__.py | 6 ++ app/handlers/subscription/autopay.py | 90 +++++++++++++++++++++++++++ app/keyboards/inline.py | 83 +++++++++++++++++++++++- app/localization/locales/en.json | 14 +++++ app/localization/locales/fa.json | 14 +++++ app/localization/locales/ru.json | 14 +++++ app/localization/locales/ua.json | 14 +++++ app/localization/locales/zh.json | 14 +++++ app/services/payment/yookassa.py | 9 ++- 10 files changed, 262 insertions(+), 2 deletions(-) diff --git a/app/handlers/balance/main.py b/app/handlers/balance/main.py index 18663d40..c3b836c5 100644 --- a/app/handlers/balance/main.py +++ b/app/handlers/balance/main.py @@ -879,3 +879,9 @@ def register_balance_handlers(dp: Dispatcher): dp.callback_query.register(handle_quick_amount_selection, F.data.startswith('quick_amount_')) dp.callback_query.register(handle_topup_amount_callback, F.data.startswith('topup_amount|')) + + from app.handlers.subscription.autopay import handle_confirm_unlink, handle_saved_cards_list, handle_unlink_card + + dp.callback_query.register(handle_saved_cards_list, F.data == 'saved_cards_list') + dp.callback_query.register(handle_unlink_card, F.data.startswith('unlink_card_')) + dp.callback_query.register(handle_confirm_unlink, F.data.startswith('confirm_unlink_')) diff --git a/app/handlers/subscription/__init__.py b/app/handlers/subscription/__init__.py index 633ef812..a9e5cd2b 100644 --- a/app/handlers/subscription/__init__.py +++ b/app/handlers/subscription/__init__.py @@ -2,8 +2,11 @@ from .autopay import ( handle_autopay_menu, + handle_confirm_unlink, + handle_saved_cards_list, handle_subscription_cancel, handle_subscription_config_back, + handle_unlink_card, set_autopay_days, show_autopay_days, toggle_autopay, @@ -157,6 +160,7 @@ __all__ = [ 'handle_app_selection', 'handle_autopay_menu', 'handle_change_devices', + 'handle_confirm_unlink', 'handle_connect_subscription', 'handle_device_guide', 'handle_device_management', @@ -172,12 +176,14 @@ __all__ = [ 'handle_promo_offer_close', 'handle_reset_devices', 'handle_reset_traffic', + 'handle_saved_cards_list', 'handle_single_device_reset', 'handle_specific_app_guide', 'handle_subscription_cancel', 'handle_subscription_config_back', 'handle_subscription_settings', 'handle_switch_traffic', + 'handle_unlink_card', 'invalidate_app_config_cache', 'load_app_config_async', 'normalize_app', diff --git a/app/handlers/subscription/autopay.py b/app/handlers/subscription/autopay.py index a99439b1..96992566 100644 --- a/app/handlers/subscription/autopay.py +++ b/app/handlers/subscription/autopay.py @@ -3,13 +3,20 @@ from aiogram.fsm.context import FSMContext from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings +from app.database.crud.saved_payment_method import ( + deactivate_payment_method, + get_active_payment_methods_by_user, +) from app.database.crud.subscription import update_subscription_autopay from app.database.models import User from app.keyboards.inline import ( + _get_payment_method_display_name, get_autopay_days_keyboard, get_autopay_keyboard, + get_confirm_unlink_keyboard, get_countries_keyboard, get_devices_keyboard, + get_saved_cards_keyboard, get_subscription_period_keyboard, get_traffic_packages_keyboard, ) @@ -134,6 +141,89 @@ async def set_autopay_days(callback: types.CallbackQuery, db_user: User, db: Asy await handle_autopay_menu(callback, db_user, db) +async def handle_saved_cards_list(callback: types.CallbackQuery, db_user: User, db: AsyncSession): + texts = get_texts(db_user.language) + cards = await get_active_payment_methods_by_user(db, db_user.id) + + if not cards: + await callback.message.edit_text( + texts.t( + 'SAVED_CARDS_EMPTY', + '💳 Привязанные карты\n\nНет привязанных карт.\n' + 'Карта привяжется автоматически при следующем пополнении баланса.', + ), + reply_markup=get_saved_cards_keyboard([], db_user.language), + parse_mode='HTML', + ) + else: + await callback.message.edit_text( + texts.t( + 'SAVED_CARDS_TITLE', + '💳 Привязанные карты\n\nВыберите карту для отвязки:', + ), + reply_markup=get_saved_cards_keyboard(cards, db_user.language), + parse_mode='HTML', + ) + await callback.answer() + + +async def handle_unlink_card(callback: types.CallbackQuery, db_user: User, db: AsyncSession): + texts = get_texts(db_user.language) + card_id = int(callback.data.split('_')[-1]) + + cards = await get_active_payment_methods_by_user(db, db_user.id) + card = next((c for c in cards if c.id == card_id), None) + + if not card: + await callback.answer( + texts.t('SAVED_CARDS_UNLINK_ERROR', '❌ Не удалось отвязать карту'), + show_alert=True, + ) + return + + card_label = _get_payment_method_display_name(card, db_user.language) + text = texts.t( + 'SAVED_CARDS_CONFIRM_UNLINK', + 'Вы уверены, что хотите отвязать карту {card}?\n\n' + 'После отвязки автоплатеж не сможет использовать эту карту.', + ).format(card=card_label) + + if len(cards) == 1: + text += texts.t( + 'SAVED_CARDS_LAST_CARD_WARNING', + '\n\n⚠️ Внимание: это ваша последняя привязанная карта. ' + 'После отвязки автоплатеж не сможет списывать средства.', + ) + + await callback.message.edit_text( + text, + reply_markup=get_confirm_unlink_keyboard(card_id, db_user.language), + parse_mode='HTML', + ) + await callback.answer() + + +async def handle_confirm_unlink(callback: types.CallbackQuery, db_user: User, db: AsyncSession): + texts = get_texts(db_user.language) + card_id = int(callback.data.split('_')[-1]) + + success = await deactivate_payment_method(db, card_id, db_user.id) + + if success: + await callback.answer( + texts.t('SAVED_CARDS_UNLINKED', '✅ Карта отвязана'), + ) + else: + await callback.answer( + texts.t('SAVED_CARDS_UNLINK_ERROR', '❌ Не удалось отвязать карту'), + show_alert=True, + ) + return + + # Return to the updated cards list + await handle_saved_cards_list(callback, db_user, db) + + async def handle_subscription_config_back( callback: types.CallbackQuery, state: FSMContext, db_user: User, db: AsyncSession ): diff --git a/app/keyboards/inline.py b/app/keyboards/inline.py index 7083e1e9..5249bce0 100644 --- a/app/keyboards/inline.py +++ b/app/keyboards/inline.py @@ -1493,6 +1493,8 @@ def get_subscription_confirm_keyboard(language: str = DEFAULT_LANGUAGE) -> Inlin def get_balance_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup: + from app.config import settings + texts = get_texts(language) keyboard = [ @@ -1500,8 +1502,17 @@ def get_balance_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMark InlineKeyboardButton(text=texts.BALANCE_HISTORY, callback_data='balance_history'), InlineKeyboardButton(text=texts.BALANCE_TOP_UP, callback_data='balance_topup'), ], - [InlineKeyboardButton(text=texts.BACK, callback_data='back_to_menu')], ] + if settings.YOOKASSA_RECURRENT_ENABLED: + keyboard.append( + [ + InlineKeyboardButton( + text=texts.t('SAVED_CARDS_BUTTON', '💳 Привязанные карты'), + callback_data='saved_cards_list', + ) + ] + ) + keyboard.append([InlineKeyboardButton(text=texts.BACK, callback_data='back_to_menu')]) return InlineKeyboardMarkup(inline_keyboard=keyboard) @@ -1937,6 +1948,76 @@ def get_autopay_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMark ) +_PAYMENT_METHOD_LOCALE_KEYS: dict[str, tuple[str, str]] = { + 'bank_card': ('PAYMENT_METHOD_BANK_CARD', '💳 Банковская карта'), + 'yoo_money': ('PAYMENT_METHOD_YOO_MONEY', '🟣 ЮMoney'), + 'sberbank': ('PAYMENT_METHOD_SBERBANK', '🟢 СберPay'), + 'tinkoff_bank': ('PAYMENT_METHOD_TINKOFF_BANK', '🟡 Т-Банк'), + 'sbp': ('PAYMENT_METHOD_SBP', '🏦 СБП'), + 'mir_pay': ('PAYMENT_METHOD_MIR_PAY', '🟦 Mir Pay'), +} + + +def _get_payment_method_display_name(card, language: str = DEFAULT_LANGUAGE) -> str: + """Локализованное название метода оплаты + реквизиты.""" + texts = get_texts(language) + + # Для банковских карт title уже содержит тип + маску (например "Visa *4444") + if card.method_type == 'bank_card' or (not card.method_type and card.card_last4): + if card.title: + return card.title + if card.card_last4: + return f'{card.card_type or "Card"} *{card.card_last4}' + + # Для остальных методов: локализованное название + реквизиты из title + locale_entry = _PAYMENT_METHOD_LOCALE_KEYS.get(card.method_type) + if locale_entry: + key, default = locale_entry + method_name = texts.t(key, default) + else: + method_name = card.method_type or 'Card' + + if card.title: + return f'{method_name} {card.title}' + return method_name + + +def get_saved_cards_keyboard( + cards: list, language: str = DEFAULT_LANGUAGE +) -> InlineKeyboardMarkup: + texts = get_texts(language) + keyboard = [] + for card in cards: + card_label = f'🗑 {_get_payment_method_display_name(card, language)}' + keyboard.append( + [InlineKeyboardButton(text=card_label, callback_data=f'unlink_card_{card.id}')] + ) + keyboard.append( + [InlineKeyboardButton(text=texts.BACK, callback_data='menu_balance')] + ) + return InlineKeyboardMarkup(inline_keyboard=keyboard) + + +def get_confirm_unlink_keyboard( + card_id: int, language: str = DEFAULT_LANGUAGE +) -> InlineKeyboardMarkup: + texts = get_texts(language) + return InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton( + text=texts.t('SAVED_CARDS_CONFIRM_YES', '✅ Да, отвязать'), + callback_data=f'confirm_unlink_{card_id}', + ), + InlineKeyboardButton( + text=texts.t('CANCEL', '❌ Отмена'), + callback_data='saved_cards_list', + ), + ] + ] + ) + + def get_autopay_days_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup: texts = get_texts(language) keyboard = [] diff --git a/app/localization/locales/en.json b/app/localization/locales/en.json index 7b18f305..5ad8b089 100644 --- a/app/localization/locales/en.json +++ b/app/localization/locales/en.json @@ -899,6 +899,20 @@ "AUTOPAY_TOGGLE_SUCCESS": "✅ Autopay {status}!", "RECURRENT_TOPUP_SUCCESS": "✅ Auto-payment completed\n\nBalance topped up by {amount} for subscription renewal.", "RECURRENT_TOPUP_FAILED": "❌ Auto-payment failed\n\nCould not charge {amount} from any saved card for subscription renewal.\n\nPlease top up your balance manually to avoid service interruption.", + "SAVED_CARDS_BUTTON": "💳 Saved cards", + "SAVED_CARDS_TITLE": "💳 Saved cards\n\nSelect a card to unlink:", + "SAVED_CARDS_EMPTY": "💳 Saved cards\n\nNo saved cards.\nA card will be saved automatically on your next balance top-up.", + "SAVED_CARDS_CONFIRM_UNLINK": "Are you sure you want to unlink {card}?\n\nAfter unlinking, autopay won't be able to use this card.", + "SAVED_CARDS_LAST_CARD_WARNING": "\n\n⚠️ Warning: this is your last saved card. After unlinking, autopay won't be able to charge payments.", + "SAVED_CARDS_UNLINKED": "✅ Card unlinked", + "SAVED_CARDS_UNLINK_ERROR": "❌ Failed to unlink card", + "SAVED_CARDS_CONFIRM_YES": "✅ Yes, unlink", + "PAYMENT_METHOD_BANK_CARD": "💳 Bank card", + "PAYMENT_METHOD_YOO_MONEY": "🟣 YooMoney", + "PAYMENT_METHOD_SBERBANK": "🟢 SberPay", + "PAYMENT_METHOD_TINKOFF_BANK": "🟡 T-Bank", + "PAYMENT_METHOD_SBP": "🏦 SBP", + "PAYMENT_METHOD_MIR_PAY": "🟦 Mir Pay", "BACK": "⬅️ Back", "BACK_BUTTON": "◀️ Back", "BACK_TO_MAIN_MENU_BUTTON": "⬅️ Back to main menu", diff --git a/app/localization/locales/fa.json b/app/localization/locales/fa.json index 98ecb71e..c0dd6d7d 100644 --- a/app/localization/locales/fa.json +++ b/app/localization/locales/fa.json @@ -919,6 +919,20 @@ "AUTOPAY_TOGGLE_SUCCESS": "✅ پرداخت خودکار {status}!", "RECURRENT_TOPUP_SUCCESS": "✅ پرداخت خودکار انجام شد\n\nموجودی به مبلغ {amount} برای تمدید اشتراک شارژ شد.", "RECURRENT_TOPUP_FAILED": "❌ پرداخت خودکار ناموفق بود\n\nامکان کسر {amount} از هیچ کارت ذخیره شده‌ای برای تمدید اشتراک وجود نداشت.\n\nلطفاً موجودی را به صورت دستی شارژ کنید.", + "SAVED_CARDS_BUTTON": "💳 کارت‌های ذخیره شده", + "SAVED_CARDS_TITLE": "💳 کارت‌های ذخیره شده\n\nکارت مورد نظر برای حذف را انتخاب کنید:", + "SAVED_CARDS_EMPTY": "💳 کارت‌های ذخیره شده\n\nکارت ذخیره شده‌ای وجود ندارد.\nکارت به صورت خودکار در شارژ بعدی موجودی ذخیره می‌شود.", + "SAVED_CARDS_CONFIRM_UNLINK": "آیا مطمئن هستید که می‌خواهید کارت {card} را حذف کنید؟\n\nپس از حذف، پرداخت خودکار نمی‌تواند از این کارت استفاده کند.", + "SAVED_CARDS_LAST_CARD_WARNING": "\n\n⚠️ توجه: این آخرین کارت ذخیره شده شماست. پس از حذف، پرداخت خودکار امکان برداشت وجه نخواهد داشت.", + "SAVED_CARDS_UNLINKED": "✅ کارت حذف شد", + "SAVED_CARDS_UNLINK_ERROR": "❌ حذف کارت انجام نشد", + "SAVED_CARDS_CONFIRM_YES": "✅ بله، حذف کن", + "PAYMENT_METHOD_BANK_CARD": "💳 کارت بانکی", + "PAYMENT_METHOD_YOO_MONEY": "🟣 ЮMoney", + "PAYMENT_METHOD_SBERBANK": "🟢 СберPay", + "PAYMENT_METHOD_TINKOFF_BANK": "🟡 Т-Банк", + "PAYMENT_METHOD_SBP": "🏦 СБП", + "PAYMENT_METHOD_MIR_PAY": "🟦 Mir Pay", "BACK": "⬅️ قبلی", "BACK_BUTTON": "◀️ بازگشت", "BACK_TO_MAIN_MENU_BUTTON": "⬅️ منوی اصلی", diff --git a/app/localization/locales/ru.json b/app/localization/locales/ru.json index 13ca4cda..addc5b6d 100644 --- a/app/localization/locales/ru.json +++ b/app/localization/locales/ru.json @@ -919,6 +919,20 @@ "AUTOPAY_TOGGLE_SUCCESS": "✅ Автоплатеж {status}!", "RECURRENT_TOPUP_SUCCESS": "✅ Автоплатёж выполнен\n\nБаланс пополнен на {amount} для продления подписки.", "RECURRENT_TOPUP_FAILED": "❌ Автоплатёж не удался\n\nНе удалось списать {amount} ни с одной сохранённой карты для продления подписки.\n\nПополните баланс вручную, чтобы подписка не прервалась.", + "SAVED_CARDS_BUTTON": "💳 Привязанные карты", + "SAVED_CARDS_TITLE": "💳 Привязанные карты\n\nВыберите карту для отвязки:", + "SAVED_CARDS_EMPTY": "💳 Привязанные карты\n\nНет привязанных карт.\nКарта привяжется автоматически при следующем пополнении баланса.", + "SAVED_CARDS_CONFIRM_UNLINK": "Вы уверены, что хотите отвязать карту {card}?\n\nПосле отвязки автоплатеж не сможет использовать эту карту.", + "SAVED_CARDS_LAST_CARD_WARNING": "\n\n⚠️ Внимание: это ваша последняя привязанная карта. После отвязки автоплатеж не сможет списывать средства.", + "SAVED_CARDS_UNLINKED": "✅ Карта отвязана", + "SAVED_CARDS_UNLINK_ERROR": "❌ Не удалось отвязать карту", + "SAVED_CARDS_CONFIRM_YES": "✅ Да, отвязать", + "PAYMENT_METHOD_BANK_CARD": "💳 Банковская карта", + "PAYMENT_METHOD_YOO_MONEY": "🟣 ЮMoney", + "PAYMENT_METHOD_SBERBANK": "🟢 СберPay", + "PAYMENT_METHOD_TINKOFF_BANK": "🟡 Т-Банк", + "PAYMENT_METHOD_SBP": "🏦 СБП", + "PAYMENT_METHOD_MIR_PAY": "🟦 Mir Pay", "BACK": "⬅️ Назад", "BACK_BUTTON": "◀️ Назад", "BACK_TO_MAIN_MENU_BUTTON": "⬅️ В главное меню", diff --git a/app/localization/locales/ua.json b/app/localization/locales/ua.json index 9de80740..2c03e93d 100644 --- a/app/localization/locales/ua.json +++ b/app/localization/locales/ua.json @@ -841,6 +841,20 @@ "AUTOPAY_TOGGLE_SUCCESS": "✅ Автоплатіж {status}!", "RECURRENT_TOPUP_SUCCESS": "✅ Автоплатіж виконано\n\nБаланс поповнено на {amount} для продовження підписки.", "RECURRENT_TOPUP_FAILED": "❌ Автоплатіж не вдався\n\nНе вдалося списати {amount} з жодної збереженої картки для продовження підписки.\n\nПоповніть баланс вручну, щоб підписка не перервалася.", + "SAVED_CARDS_BUTTON": "💳 Прив'язані картки", + "SAVED_CARDS_TITLE": "💳 Прив'язані картки\n\nОберіть картку для відв'язки:", + "SAVED_CARDS_EMPTY": "💳 Прив'язані картки\n\nНемає прив'язаних карток.\nКартка прив'яжеться автоматично при наступному поповненні балансу.", + "SAVED_CARDS_CONFIRM_UNLINK": "Ви впевнені, що хочете відв'язати картку {card}?\n\nПісля відв'язки автоплатіж не зможе використовувати цю картку.", + "SAVED_CARDS_LAST_CARD_WARNING": "\n\n⚠️ Увага: це ваша остання прив'язана картка. Після відв'язки автоплатіж не зможе списувати кошти.", + "SAVED_CARDS_UNLINKED": "✅ Картку відв'язано", + "SAVED_CARDS_UNLINK_ERROR": "❌ Не вдалося відв'язати картку", + "SAVED_CARDS_CONFIRM_YES": "✅ Так, відв'язати", + "PAYMENT_METHOD_BANK_CARD": "💳 Банківська картка", + "PAYMENT_METHOD_YOO_MONEY": "🟣 ЮMoney", + "PAYMENT_METHOD_SBERBANK": "🟢 СберPay", + "PAYMENT_METHOD_TINKOFF_BANK": "🟡 Т-Банк", + "PAYMENT_METHOD_SBP": "🏦 СБП", + "PAYMENT_METHOD_MIR_PAY": "🟦 Mir Pay", "BACK": "⬅️ Назад", "BACK_TO_MAIN_MENU_BUTTON": "⬅️ В головне меню", "BACK_TO_MENU": "🏠 В головне меню", diff --git a/app/localization/locales/zh.json b/app/localization/locales/zh.json index aba8f49a..4a8e0ce3 100644 --- a/app/localization/locales/zh.json +++ b/app/localization/locales/zh.json @@ -839,6 +839,20 @@ "AUTOPAY_TOGGLE_SUCCESS": "✅自动支付{status}!", "RECURRENT_TOPUP_SUCCESS": "✅ 自动扣款成功\n\n余额已充值{amount},用于续订订阅。", "RECURRENT_TOPUP_FAILED": "❌ 自动扣款失败\n\n无法从任何已保存的银行卡中扣除{amount}以续订订阅。\n\n请手动充值余额以避免服务中断。", +"SAVED_CARDS_BUTTON": "💳 已绑定的卡", +"SAVED_CARDS_TITLE": "💳 已绑定的卡\n\n选择要解绑的卡:", +"SAVED_CARDS_EMPTY": "💳 已绑定的卡\n\n没有已绑定的卡。\n下次充值余额时将自动绑定。", +"SAVED_CARDS_CONFIRM_UNLINK": "确定要解绑 {card} 吗?\n\n解绑后自动扣款将无法使用此卡。", +"SAVED_CARDS_LAST_CARD_WARNING": "\n\n⚠️ 注意:这是您最后一张绑定的卡。解绑后自动扣款将无法进行。", +"SAVED_CARDS_UNLINKED": "✅ 卡已解绑", +"SAVED_CARDS_UNLINK_ERROR": "❌ 解绑失败", +"SAVED_CARDS_CONFIRM_YES": "✅ 确认解绑", +"PAYMENT_METHOD_BANK_CARD": "💳 银行卡", +"PAYMENT_METHOD_YOO_MONEY": "🟣 ЮMoney", +"PAYMENT_METHOD_SBERBANK": "🟢 СберPay", +"PAYMENT_METHOD_TINKOFF_BANK": "🟡 Т-Банк", +"PAYMENT_METHOD_SBP": "🏦 СБП", +"PAYMENT_METHOD_MIR_PAY": "🟦 Mir Pay", "BACK": "⬅️返回", "BACK_TO_MAIN_MENU_BUTTON": "⬅️返回主菜单", "BACK_TO_MENU": "🏠返回主菜单", diff --git a/app/services/payment/yookassa.py b/app/services/payment/yookassa.py index 035c4b92..0865fe6f 100644 --- a/app/services/payment/yookassa.py +++ b/app/services/payment/yookassa.py @@ -1140,11 +1140,18 @@ class YooKassaPaymentMixin: expiry_year = str(raw_year) if raw_year is not None else None method_type = pm.get('type', 'bank_card') - # Формируем название + # Формируем title — только реквизиты без названия метода + # (локализованное название подставляется в UI через _get_payment_method_display_name) title = None if card_last4: type_label = card_type or 'Card' title = f'{type_label} *{card_last4}' + elif method_type != 'bank_card': + # Для не-карточных методов: yoo_money (account_number), sbp/sberbank (phone) и т.д. + account = pm.get('account_number') or pm.get('phone') + if account: + masked = account[-4:] if len(account) >= 4 else account + title = f'*{masked}' saved = await create_saved_payment_method( db=db, From 1badb39c4981f3680bc37cc3289c2349a74a74c2 Mon Sep 17 00:00:00 2001 From: firewookie Date: Thu, 12 Mar 2026 09:35:07 +0500 Subject: [PATCH 2/4] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=BE=20=D0=B8=D0=BC=D0=BF=D0=BE=D1=80=D1=82=D0=B0=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/handlers/balance/main.py | 3 +-- app/keyboards/inline.py | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/app/handlers/balance/main.py b/app/handlers/balance/main.py index c3b836c5..de032223 100644 --- a/app/handlers/balance/main.py +++ b/app/handlers/balance/main.py @@ -8,6 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings from app.database.crud.transaction import get_user_transactions from app.database.models import TransactionType, User +from app.handlers.subscription.autopay import handle_confirm_unlink, handle_saved_cards_list, handle_unlink_card from app.keyboards.inline import ( get_back_keyboard, get_balance_keyboard, @@ -880,8 +881,6 @@ def register_balance_handlers(dp: Dispatcher): dp.callback_query.register(handle_topup_amount_callback, F.data.startswith('topup_amount|')) - from app.handlers.subscription.autopay import handle_confirm_unlink, handle_saved_cards_list, handle_unlink_card - dp.callback_query.register(handle_saved_cards_list, F.data == 'saved_cards_list') dp.callback_query.register(handle_unlink_card, F.data.startswith('unlink_card_')) dp.callback_query.register(handle_confirm_unlink, F.data.startswith('confirm_unlink_')) diff --git a/app/keyboards/inline.py b/app/keyboards/inline.py index 5249bce0..0a5bdfd6 100644 --- a/app/keyboards/inline.py +++ b/app/keyboards/inline.py @@ -1493,8 +1493,6 @@ def get_subscription_confirm_keyboard(language: str = DEFAULT_LANGUAGE) -> Inlin def get_balance_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup: - from app.config import settings - texts = get_texts(language) keyboard = [ From 673afccb8c19860f6f71112e8dd10bcbb25838a4 Mon Sep 17 00:00:00 2001 From: firewookie Date: Thu, 12 Mar 2026 09:35:54 +0500 Subject: [PATCH 3/4] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=BB=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/cabinet/routes/admin_tariffs.py | 4 +--- app/cabinet/routes/admin_users.py | 12 +++++++----- app/database/models.py | 4 +++- app/keyboards/inline.py | 16 ++++------------ 4 files changed, 15 insertions(+), 21 deletions(-) diff --git a/app/cabinet/routes/admin_tariffs.py b/app/cabinet/routes/admin_tariffs.py index 9fb5de52..fff76713 100644 --- a/app/cabinet/routes/admin_tariffs.py +++ b/app/cabinet/routes/admin_tariffs.py @@ -708,9 +708,7 @@ async def sync_tariff_squads( ) if consecutive_failures >= _SYNC_SQUADS_MAX_CONSECUTIVE_FAILURES: aborted = True - errors.append( - f'Aborted after {_SYNC_SQUADS_MAX_CONSECUTIVE_FAILURES} consecutive failures' - ) + errors.append(f'Aborted after {_SYNC_SQUADS_MAX_CONSECUTIVE_FAILURES} consecutive failures') return 'error' await asyncio.gather(*[_sync_one(sub) for sub in subscriptions]) diff --git a/app/cabinet/routes/admin_users.py b/app/cabinet/routes/admin_users.py index 06311e72..63f06cfc 100644 --- a/app/cabinet/routes/admin_users.py +++ b/app/cabinet/routes/admin_users.py @@ -1644,11 +1644,13 @@ async def update_user_promo_group( await db.execute(sa_delete(UserPromoGroup).where(UserPromoGroup.user_id == user_id)) if new_promo_group_id is not None: - db.add(UserPromoGroup( - user_id=user_id, - promo_group_id=new_promo_group_id, - assigned_by='admin', - )) + db.add( + UserPromoGroup( + user_id=user_id, + promo_group_id=new_promo_group_id, + assigned_by='admin', + ) + ) await db.flush() await sync_user_primary_promo_group(db, user_id) diff --git a/app/database/models.py b/app/database/models.py index 7abfa79b..49702a1e 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -1147,7 +1147,9 @@ class User(Base): discord_id = Column(String(255), unique=True, nullable=True, index=True) vk_id = Column(BigInteger, unique=True, nullable=True, index=True) broadcasts = relationship('BroadcastHistory', back_populates='admin') - referrals = relationship('User', backref='referrer', remote_side=[id], foreign_keys='User.referred_by_id', post_update=True) + referrals = relationship( + 'User', backref='referrer', remote_side=[id], foreign_keys='User.referred_by_id', post_update=True + ) subscription = relationship('Subscription', back_populates='user', uselist=False) transactions = relationship('Transaction', back_populates='user') referral_earnings = relationship('ReferralEarning', foreign_keys='ReferralEarning.user_id', back_populates='user') diff --git a/app/keyboards/inline.py b/app/keyboards/inline.py index 0a5bdfd6..bb02b88e 100644 --- a/app/keyboards/inline.py +++ b/app/keyboards/inline.py @@ -1980,25 +1980,17 @@ def _get_payment_method_display_name(card, language: str = DEFAULT_LANGUAGE) -> return method_name -def get_saved_cards_keyboard( - cards: list, language: str = DEFAULT_LANGUAGE -) -> InlineKeyboardMarkup: +def get_saved_cards_keyboard(cards: list, language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup: texts = get_texts(language) keyboard = [] for card in cards: card_label = f'🗑 {_get_payment_method_display_name(card, language)}' - keyboard.append( - [InlineKeyboardButton(text=card_label, callback_data=f'unlink_card_{card.id}')] - ) - keyboard.append( - [InlineKeyboardButton(text=texts.BACK, callback_data='menu_balance')] - ) + keyboard.append([InlineKeyboardButton(text=card_label, callback_data=f'unlink_card_{card.id}')]) + keyboard.append([InlineKeyboardButton(text=texts.BACK, callback_data='menu_balance')]) return InlineKeyboardMarkup(inline_keyboard=keyboard) -def get_confirm_unlink_keyboard( - card_id: int, language: str = DEFAULT_LANGUAGE -) -> InlineKeyboardMarkup: +def get_confirm_unlink_keyboard(card_id: int, language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup: texts = get_texts(language) return InlineKeyboardMarkup( inline_keyboard=[ From 2f5674fcd7b74b1f143b58dd371d3e43f7f3a829 Mon Sep 17 00:00:00 2001 From: firewookie Date: Thu, 12 Mar 2026 09:40:29 +0500 Subject: [PATCH 4/4] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=BB=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/cabinet/routes/admin_users.py | 4 +++- app/handlers/admin/users.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/cabinet/routes/admin_users.py b/app/cabinet/routes/admin_users.py index f6a7c4ff..e36c0b3f 100644 --- a/app/cabinet/routes/admin_users.py +++ b/app/cabinet/routes/admin_users.py @@ -1136,7 +1136,9 @@ async def update_user_subscription( new_base = tariff.device_limit or 1 new_total = new_base + extra_devices # Cap at new tariff's max_device_limit, falling back to global MAX_DEVICES_LIMIT - effective_max = tariff.max_device_limit or (settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else None) + effective_max = tariff.max_device_limit or ( + settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else None + ) if effective_max and new_total > effective_max: new_total = effective_max subscription.device_limit = new_total diff --git a/app/handlers/admin/users.py b/app/handlers/admin/users.py index 0f995674..dd8527c7 100644 --- a/app/handlers/admin/users.py +++ b/app/handlers/admin/users.py @@ -5329,7 +5329,9 @@ async def confirm_admin_tariff_change(callback: types.CallbackQuery, db_user: Us new_base = tariff.device_limit or 1 new_total = new_base + extra_devices - effective_max = tariff.max_device_limit or (settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else None) + effective_max = tariff.max_device_limit or ( + settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else None + ) if effective_max and new_total > effective_max: new_total = effective_max subscription.device_limit = new_total