From 1f47f723d0a31669c7ef468c73bf817e3c6b891b Mon Sep 17 00:00:00 2001 From: Boris Kovalskii <36034823+JustYay@users.noreply.github.com> Date: Sat, 7 Feb 2026 08:33:24 +1000 Subject: [PATCH 1/3] Add providers order --- handlers/payments/fast_payment_flow.py | 21 ++++++++++------ handlers/payments/providers.py | 34 ++++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/handlers/payments/fast_payment_flow.py b/handlers/payments/fast_payment_flow.py index 4e36f87b..2e9b25f0 100644 --- a/handlers/payments/fast_payment_flow.py +++ b/handlers/payments/fast_payment_flow.py @@ -27,7 +27,7 @@ from handlers.payments.currency_flow import ( shortfall_lead_text, currency_label, ) -from handlers.payments.providers import get_providers_with_hooks +from handlers.payments.providers import get_providers_with_hooks, sort_provider_names from handlers.texts import FAST_PAY_CHOOSE_CURRENCY, FAST_PAY_CHOOSE_PROVIDER, FASTFLOW_COUPON_APPLIED_TEMPLATE from handlers.utils import edit_or_send_message from logger import logger @@ -115,6 +115,7 @@ async def try_fast_payment_flow( cfg = providers_map.get(p_up) or {} if cfg.get("fast") and cfg.get("enabled", True): providers.append(p_up) + providers = sort_provider_names(providers, providers_map) mode, one_screen = get_currency_mode() multicurrency_mode = mode == "RUB+USD" @@ -425,6 +426,7 @@ async def fastflow_apply_coupon(message: Message, state: FSMContext, session: An cfg = providers_map.get(p_up) or {} if cfg.get("fast") and cfg.get("enabled", True): providers.append(p_up) + providers = sort_provider_names(providers, providers_map) mode, one_screen = get_currency_mode() multicurrency_mode = mode == "RUB+USD" @@ -541,13 +543,16 @@ async def choose_payment_currency(callback_query: CallbackQuery, state: FSMConte configured = [str(p) for p in (USE_NEW_PAYMENT_FLOW or [])] providers = [p.upper() for p in configured] - filtered = [ - provider_upper - for provider_upper in (p.upper() for p in providers) - if (providers_map.get(provider_upper) or {}).get("currency") == currency - and (providers_map.get(provider_upper) or {}).get("fast") - and (providers_map.get(provider_upper) or {}).get("enabled", True) - ] + filtered = sort_provider_names( + [ + provider_upper + for provider_upper in (p.upper() for p in providers) + if (providers_map.get(provider_upper) or {}).get("currency") == currency + and (providers_map.get(provider_upper) or {}).get("fast") + and (providers_map.get(provider_upper) or {}).get("enabled", True) + ], + providers_map, + ) await state.update_data(chosen_currency=currency) if not filtered: diff --git a/handlers/payments/providers.py b/handlers/payments/providers.py index c1c67a13..4db18c55 100644 --- a/handlers/payments/providers.py +++ b/handlers/payments/providers.py @@ -2,63 +2,93 @@ from typing import Any from hooks.hooks import run_hooks - PROVIDERS_BASE: dict[str, dict[str, Any]] = { "YOOKASSA": { "currency": "RUB", "value": "pay_yookassa", "fast": "process_custom_amount_input", + "order": 10, }, "YOOMONEY": { "currency": "RUB", "value": "pay_yoomoney", "fast": "process_custom_amount_input_yoomoney", + "order": 20, }, "ROBOKASSA": { "currency": "RUB", "value": "pay_robokassa", "fast": "handle_custom_amount_input", + "order": 30, }, "KASSAI_CARDS": { "currency": "RUB", "value": "pay_kassai_cards", "fast": "handle_custom_amount_input_kassai_cards", "module": "kassai", + "order": 40, }, "KASSAI_SBP": { "currency": "RUB", "value": "pay_kassai_sbp", "fast": "handle_custom_amount_input_kassai_sbp", "module": "kassai", + "order": 50, }, "TRIBUTE": { "currency": "RUB+USD", "value": "pay_tribute", "fast": None, + "order": 60, }, "HELEKET": { "currency": "USD", "value": "pay_heleket_crypto", "fast": "handle_custom_amount_input_heleket", + "order": 70, }, "CRYPTOBOT": { "currency": "USD", "value": "pay_cryptobot", "fast": "process_custom_amount_input", + "order": 80, }, "FREEKASSA": { "currency": "USD", "value": "pay_freekassa", "fast": None, + "order": 90, }, "STARS": { "currency": "STARS", "value": "pay_stars", "fast": "process_custom_amount_input_stars", + "order": 100, }, } +def _get_effective_order(name: str, cfg: dict[str, Any]) -> int: + """Возвращает эффективный порядок провайдера (админ > модуль > дефолт).""" + from core.settings.providers_order_config import PROVIDERS_ORDER + + if name in PROVIDERS_ORDER: + return PROVIDERS_ORDER[name] + return cfg.get("order", 999) + + +def _sort_providers(providers: dict[str, dict[str, Any]]) -> dict[str, dict[str, Any]]: + """Сортирует провайдеров по полю 'order' (меньше = выше).""" + return dict( + sorted(providers.items(), key=lambda item: _get_effective_order(item[0], item[1])) + ) + + +def sort_provider_names(names: list[str], providers_map: dict[str, dict[str, Any]]) -> list[str]: + """Сортирует список имён провайдеров по их 'order' из providers_map.""" + return sorted(names, key=lambda n: _get_effective_order(n, providers_map.get(n) or {})) + + def get_providers(flags: dict[str, bool]) -> dict[str, dict[str, Any]]: providers: dict[str, dict[str, Any]] = {} for name, base in PROVIDERS_BASE.items(): @@ -81,4 +111,4 @@ async def get_providers_with_hooks(flags: dict[str, bool]) -> dict[str, dict[str base = dict(providers.get(name, {})) base.update(patch) providers[name] = base - return providers + return _sort_providers(providers) From 4372e651b92a9fe5519f1b9ab528b8254f914cc0 Mon Sep 17 00:00:00 2001 From: Boris Kovalskii <36034823+JustYay@users.noreply.github.com> Date: Sat, 7 Feb 2026 08:34:16 +1000 Subject: [PATCH 2/3] Provider order to core --- core/bootstrap.py | 2 ++ core/settings/providers_order_config.py | 38 +++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 core/settings/providers_order_config.py diff --git a/core/bootstrap.py b/core/bootstrap.py index 497e993b..e68d98b0 100644 --- a/core/bootstrap.py +++ b/core/bootstrap.py @@ -6,6 +6,7 @@ from .settings.modes_config import MODES_CONFIG, load_modes_config, update_modes from .settings.money_config import MONEY_CONFIG, load_money_config, update_money_config from .settings.notifications_config import NOTIFICATIONS_CONFIG, load_notifications_config, update_notifications_config from .settings.payments_config import PAYMENTS_CONFIG, load_payments_config, update_payments_config +from .settings.providers_order_config import PROVIDERS_ORDER, load_providers_order, update_providers_order from .settings.tariffs_config import TARIFFS_CONFIG, load_tariffs_config, update_tariffs_config @@ -15,6 +16,7 @@ async def bootstrap() -> None: await load_notifications_config(session) await load_modes_config(session) await load_payments_config(session) + await load_providers_order(session) await load_money_config(session) await load_management_config(session) await load_tariffs_config(session) diff --git a/core/settings/providers_order_config.py b/core/settings/providers_order_config.py new file mode 100644 index 00000000..25bd06fd --- /dev/null +++ b/core/settings/providers_order_config.py @@ -0,0 +1,38 @@ +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from database.models import Setting + +PROVIDERS_ORDER: dict[str, int] = {} + + +async def load_providers_order(session: AsyncSession) -> None: + stmt = select(Setting).where(Setting.key == "PROVIDERS_ORDER") + result = await session.execute(stmt) + setting = result.scalar_one_or_none() + + PROVIDERS_ORDER.clear() + if setting and isinstance(setting.value, dict): + PROVIDERS_ORDER.update({k: int(v) for k, v in setting.value.items()}) + await session.flush() + + +async def update_providers_order(session: AsyncSession, new_order: dict[str, int]) -> None: + stmt = select(Setting).where(Setting.key == "PROVIDERS_ORDER") + result = await session.execute(stmt) + setting = result.scalar_one_or_none() + + if setting is None: + setting = Setting( + key="PROVIDERS_ORDER", + value=new_order, + description="Порядок отображения платёжных провайдеров", + ) + session.add(setting) + else: + setting.value = new_order + + await session.commit() + + PROVIDERS_ORDER.clear() + PROVIDERS_ORDER.update(new_order) From 14b97a9c3d7be9a019199ebd1567f910f2a4889f Mon Sep 17 00:00:00 2001 From: Boris Kovalskii <36034823+JustYay@users.noreply.github.com> Date: Sat, 7 Feb 2026 08:36:03 +1000 Subject: [PATCH 3/3] Providers order to admin --- handlers/admin/settings/keyboard.py | 49 ++++++++++ handlers/admin/settings/settings_cashboxes.py | 95 ++++++++++++++++++- 2 files changed, 143 insertions(+), 1 deletion(-) diff --git a/handlers/admin/settings/keyboard.py b/handlers/admin/settings/keyboard.py index 02938277..8c0c0bd6 100644 --- a/handlers/admin/settings/keyboard.py +++ b/handlers/admin/settings/keyboard.py @@ -97,15 +97,64 @@ def build_settings_buttons_kb(buttons_state: dict[str, bool]) -> InlineKeyboardM def build_settings_cashboxes_kb(providers_state: dict[str, bool]) -> InlineKeyboardMarkup: + order_button = InlineKeyboardButton( + text="📋 Порядок касс", + callback_data=AdminPanelCallback(action="settings_providers_order").pack(), + ) return build_toggle_section_keyboard( titles=PAYMENT_PROVIDER_TITLES, state=providers_state, action="settings_cashbox_toggle", columns=2, back_action="settings", + extra_rows=[[order_button]], ) +def build_providers_order_kb(sorted_names: list[str]) -> InlineKeyboardMarkup: + """Клавиатура для управления порядком отображения касс.""" + builder = InlineKeyboardBuilder() + + for idx, name in enumerate(sorted_names): + title = PAYMENT_PROVIDER_TITLES.get(name, name) + pos = idx + 1 + builder.row( + InlineKeyboardButton( + text="⬆️", + callback_data=AdminPanelCallback( + action="settings_order_up", page=pos, + ).pack(), + ), + InlineKeyboardButton( + text=f"{pos}. {title}", + callback_data=AdminPanelCallback( + action="settings_providers_order", + ).pack(), + ), + InlineKeyboardButton( + text="⬇️", + callback_data=AdminPanelCallback( + action="settings_order_down", page=pos, + ).pack(), + ), + ) + + builder.row( + InlineKeyboardButton( + text="🔄 Сбросить порядок", + callback_data=AdminPanelCallback(action="settings_order_reset").pack(), + ) + ) + builder.row( + InlineKeyboardButton( + text="Назад", + callback_data=AdminPanelCallback(action="settings_cashboxes").pack(), + ) + ) + + return builder.as_markup() + + def build_settings_notifications_kb(notifications_state: dict[str, object]) -> InlineKeyboardMarkup: intervals_button = InlineKeyboardButton( text="Интервалы", diff --git a/handlers/admin/settings/settings_cashboxes.py b/handlers/admin/settings/settings_cashboxes.py index 548683b3..4e476a82 100644 --- a/handlers/admin/settings/settings_cashboxes.py +++ b/handlers/admin/settings/settings_cashboxes.py @@ -3,10 +3,12 @@ from aiogram.types import CallbackQuery from sqlalchemy.ext.asyncio import AsyncSession from core.bootstrap import PAYMENTS_CONFIG, update_payments_config +from core.settings.providers_order_config import PROVIDERS_ORDER, update_providers_order from filters.admin import IsAdminFilter +from handlers.payments.providers import PROVIDERS_BASE, _get_effective_order from ..panel.keyboard import AdminPanelCallback -from .keyboard import PAYMENT_PROVIDER_TITLES, build_settings_cashboxes_kb +from .keyboard import PAYMENT_PROVIDER_TITLES, build_providers_order_kb, build_settings_cashboxes_kb router = Router(name="admin_settings_cashboxes") @@ -18,6 +20,15 @@ async def load_payment_providers_settings() -> dict[str, bool]: return {k: bool(config.get(k, False)) for k in PAYMENT_PROVIDER_TITLES.keys()} +def _get_sorted_provider_names() -> list[str]: + """Возвращает все провайдеры, отсортированные по текущему порядку.""" + all_names = list(PROVIDERS_BASE.keys()) + return sorted( + all_names, + key=lambda n: _get_effective_order(n, PROVIDERS_BASE.get(n, {})), + ) + + @router.callback_query(AdminPanelCallback.filter(F.action == "settings_cashboxes")) async def open_settings_cashboxes_menu(callback: CallbackQuery, session: AsyncSession) -> None: providers_state = await load_payment_providers_settings() @@ -56,3 +67,85 @@ async def toggle_cashbox_setting( reply_markup=build_settings_cashboxes_kb(updated_state), ) await callback.answer("Настройка обновлена") + +@router.callback_query(AdminPanelCallback.filter(F.action == "settings_providers_order")) +async def open_providers_order_menu(callback: CallbackQuery, session: AsyncSession) -> None: + sorted_names = _get_sorted_provider_names() + text = ( + "📋 Порядок отображения касс\n\n" + "⬆️ — поднять выше\n" + "⬇️ — опустить ниже\n\n" + "Порядок влияет на меню оплаты и fast flow." + ) + await callback.message.edit_text( + text=text, + reply_markup=build_providers_order_kb(sorted_names), + ) + await callback.answer() + + +@router.callback_query(AdminPanelCallback.filter(F.action == "settings_order_up")) +async def move_provider_up( + callback: CallbackQuery, + callback_data: AdminPanelCallback, + session: AsyncSession, +) -> None: + sorted_names = _get_sorted_provider_names() + idx = callback_data.page - 1 + + if idx <= 0: + await callback.answer("Уже на первом месте", show_alert=False) + return + + if idx >= len(sorted_names): + await callback.answer("Неизвестная касса", show_alert=True) + return + + sorted_names[idx], sorted_names[idx - 1] = sorted_names[idx - 1], sorted_names[idx] + + new_order = {name: (i + 1) * 10 for i, name in enumerate(sorted_names)} + await update_providers_order(session, new_order) + + await callback.message.edit_reply_markup( + reply_markup=build_providers_order_kb(sorted_names), + ) + await callback.answer("✅ Перемещено выше") + + +@router.callback_query(AdminPanelCallback.filter(F.action == "settings_order_down")) +async def move_provider_down( + callback: CallbackQuery, + callback_data: AdminPanelCallback, + session: AsyncSession, +) -> None: + sorted_names = _get_sorted_provider_names() + idx = callback_data.page - 1 + + if idx >= len(sorted_names) - 1: + await callback.answer("Уже на последнем месте", show_alert=False) + return + + if idx < 0: + await callback.answer("Неизвестная касса", show_alert=True) + return + + sorted_names[idx], sorted_names[idx + 1] = sorted_names[idx + 1], sorted_names[idx] + + new_order = {name: (i + 1) * 10 for i, name in enumerate(sorted_names)} + await update_providers_order(session, new_order) + + await callback.message.edit_reply_markup( + reply_markup=build_providers_order_kb(sorted_names), + ) + await callback.answer("✅ Перемещено ниже") + + +@router.callback_query(AdminPanelCallback.filter(F.action == "settings_order_reset")) +async def reset_providers_order(callback: CallbackQuery, session: AsyncSession) -> None: + await update_providers_order(session, {}) + + sorted_names = _get_sorted_provider_names() + await callback.message.edit_reply_markup( + reply_markup=build_providers_order_kb(sorted_names), + ) + await callback.answer("✅ Порядок сброшен на дефолтный")