Files

388 lines
15 KiB
Python

from aiogram import F
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from core.settings.tariffs_config import normalize_tariff_config
from database.models import Tariff
from filters.admin import IsAdminFilter
from .. import router
from ..keyboard import AdminTariffCallback
class TariffConfigState(StatesGroup):
choosing_section = State()
entering_devices = State()
entering_traffic = State()
entering_device_step = State()
entering_device_overrides = State()
entering_traffic_step = State()
entering_traffic_overrides = State()
def build_config_menu_kb(tariff_id: int) -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="📱 Варианты устройств",
callback_data=f"cfg_edit_devices|{tariff_id}",
)
],
[
InlineKeyboardButton(
text="📦 Варианты трафика",
callback_data=f"cfg_edit_traffic|{tariff_id}",
)
],
[
InlineKeyboardButton(
text="💰 Шаг доплаты за устройства",
callback_data=f"cfg_edit_device_step|{tariff_id}",
)
],
[
InlineKeyboardButton(
text="📊 Доплаты по вариантам устройств",
callback_data=f"cfg_edit_device_over|{tariff_id}",
)
],
[
InlineKeyboardButton(
text="💰 Шаг доплаты за трафик (ГБ)",
callback_data=f"cfg_edit_traffic_step|{tariff_id}",
)
],
[
InlineKeyboardButton(
text="📊 Доплаты по вариантам трафика",
callback_data=f"cfg_edit_traffic_over|{tariff_id}",
)
],
[
InlineKeyboardButton(
text="⬅️ Назад к тарифу",
callback_data=AdminTariffCallback(action=f"view|{tariff_id}").pack(),
)
],
]
)
def build_cancel_config_kb(tariff_id: int) -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="❌ Отмена",
callback_data=f"edit_config|{tariff_id}",
)
]
]
)
def calculate_device_formula_extra(tariff: Tariff, devices: int) -> int:
base_devices = tariff.device_limit
step = getattr(tariff, "device_step_rub", None) or 0
if base_devices is None or devices <= base_devices:
return 0
return (devices - base_devices) * step
def calculate_traffic_formula_extra(tariff: Tariff, gb_value: int) -> int:
base_traffic = tariff.traffic_limit
step = getattr(tariff, "traffic_step_rub", None) or 0
if gb_value == 0:
return 0
if base_traffic is None or gb_value <= base_traffic:
return 0
return (gb_value - base_traffic) * step
def build_device_overrides_screen(tariff: Tariff) -> tuple[str, InlineKeyboardMarkup]:
tariff_id = tariff.id
base_price = int(tariff.price_rub or 0)
device_options = tariff.device_options or []
overrides = getattr(tariff, "device_overrides", None) or {}
lines: list[str] = []
lines.append("📊 Доплаты по вариантам устройств.")
lines.append("")
lines.append(f"Базовая цена тарифа: <b>{base_price}₽</b>")
lines.append("Ниже показаны варианты устройств и текущая доплата.")
lines.append("Значение <code>0</code> можно использовать как безлимит по устройствам.")
lines.append("Нажмите на вариант, чтобы задать доплату в рублях.")
lines.append("Отправьте <code>0</code>, чтобы вернуть расчёт по базовому шагу.")
lines.append("")
lines.append("Текущие значения:")
for devices in sorted(device_options):
key = str(devices)
formula_extra = calculate_device_formula_extra(tariff, devices)
override_extra = overrides.get(key)
if override_extra is not None:
effective_extra = int(override_extra)
status = " (индивидуальная доплата)"
else:
effective_extra = formula_extra
status = ""
if devices == 0:
label = "безлимит устройств"
else:
label = f"{devices} устр."
lines.append(f"{label}: доплата {effective_extra}{status}")
text = "\n".join(lines)
rows: list[list[InlineKeyboardButton]] = []
for devices in sorted(device_options):
key = str(devices)
formula_extra = calculate_device_formula_extra(tariff, devices)
override_extra = overrides.get(key)
if override_extra is not None:
effective_extra = int(override_extra)
status = ""
else:
effective_extra = formula_extra
status = ""
if devices == 0:
name = "безлимит устройств"
else:
name = f"{devices} устр."
if effective_extra > 0:
label = f"{status} {name} — доплата +{effective_extra}"
else:
label = f"{status} {name} — без доплаты"
label = label.strip()
rows.append([
InlineKeyboardButton(
text=label,
callback_data=f"cfg_dev_over_item|{tariff_id}|{devices}",
)
])
rows.append([
InlineKeyboardButton(
text="🧹 Сбросить все индивидуальные доплаты",
callback_data=f"cfg_dev_over_clear|{tariff_id}",
)
])
rows.append([
InlineKeyboardButton(
text="⬅️ Назад к конфигуратору",
callback_data=f"edit_config|{tariff_id}",
)
])
markup = InlineKeyboardMarkup(inline_keyboard=rows)
return text, markup
def build_traffic_overrides_screen(tariff: Tariff) -> tuple[str, InlineKeyboardMarkup]:
tariff_id = tariff.id
base_price = int(tariff.price_rub or 0)
traffic_options = tariff.traffic_options_gb or []
overrides = getattr(tariff, "traffic_overrides", None) or {}
all_options = sorted(set(traffic_options + [0]))
lines: list[str] = []
lines.append("📊 Доплаты по вариантам трафика.")
lines.append("")
lines.append(f"Базовая цена тарифа: <b>{base_price}₽</b>")
lines.append("Ниже показаны варианты лимитов и текущая доплата.")
lines.append("Значение <code>0</code> — безлимитный трафик.")
lines.append("Нажмите на вариант, чтобы задать доплату в рублях.")
lines.append("Отправьте <code>0</code>, чтобы вернуть расчёт по базовому шагу.")
lines.append("")
lines.append("Текущие значения:")
for gb in all_options:
key = str(gb)
formula_extra = calculate_traffic_formula_extra(tariff, gb)
override_extra = overrides.get(key)
if override_extra is not None:
effective_extra = int(override_extra)
status = " (индивидуальная доплата)"
else:
effective_extra = formula_extra
status = ""
if gb == 0:
label = "безлимит"
else:
label = f"{gb} ГБ"
lines.append(f"{label}: доплата {effective_extra}{status}")
text = "\n".join(lines)
rows: list[list[InlineKeyboardButton]] = []
for gb in all_options:
key = str(gb)
formula_extra = calculate_traffic_formula_extra(tariff, gb)
override_extra = overrides.get(key)
if override_extra is not None:
effective_extra = int(override_extra)
status = ""
else:
effective_extra = formula_extra
status = ""
if gb == 0:
name = "безлимит"
else:
name = f"{gb} ГБ"
if effective_extra > 0:
label = f"{status} {name} — доплата +{effective_extra}"
else:
label = f"{status} {name} — без доплаты"
label = label.strip()
rows.append([
InlineKeyboardButton(
text=label,
callback_data=f"cfg_trf_over_item|{tariff_id}|{gb}",
)
])
rows.append([
InlineKeyboardButton(
text="🧹 Сбросить все индивидуальные доплаты",
callback_data=f"cfg_trf_over_clear|{tariff_id}",
)
])
rows.append([
InlineKeyboardButton(
text="⬅️ Назад к конфигуратору",
callback_data=f"edit_config|{tariff_id}",
)
])
markup = InlineKeyboardMarkup(inline_keyboard=rows)
return text, markup
def build_config_summary_text(tariff: Tariff) -> str:
cfg = normalize_tariff_config(tariff.to_dict())
configurable_text = "включен" if getattr(tariff, "configurable", False) else "выключен"
base_duration = tariff.duration_days
base_devices = tariff.device_limit if tariff.device_limit is not None else ""
if tariff.traffic_limit is None:
base_traffic_text = "безлимит"
else:
base_traffic_text = f"{tariff.traffic_limit} ГБ"
base_price = tariff.price_rub or 0
device_options = cfg.get("device_options") or []
traffic_options_gb = cfg.get("traffic_options_gb")
duration_line = f"📅 Длительность: фиксированная, {base_duration} дн."
if device_options:
devices_parts = []
for d in device_options:
if d == 0:
devices_parts.append("безлимит")
else:
devices_parts.append(str(d))
devices_str = ", ".join(devices_parts)
devices_line = f"📱 Устройства: варианты — {devices_str}"
else:
devices_line = f"📱 Устройства: выбор отключён, по умолчанию {base_devices}"
if traffic_options_gb is None:
traffic_line = "📦 Трафик: выбор трафика отключён"
else:
traffic_parts = []
for g in traffic_options_gb:
if g == 0:
traffic_parts.append("безлимит")
else:
traffic_parts.append(f"{g} ГБ")
traffic_str = ", ".join(traffic_parts)
traffic_line = f"📦 Трафик: варианты — {traffic_str}"
device_step = getattr(tariff, "device_step_rub", None) or 0
traffic_step = getattr(tariff, "traffic_step_rub", None) or 0
device_overrides = getattr(tariff, "device_overrides", None) or {}
traffic_overrides = getattr(tariff, "traffic_overrides", None) or {}
base_block = (
"<blockquote>"
"🎯 База тарифа:\n"
f"• Длительность: <b>{base_duration} дней</b>\n"
f"• Устройства: <b>{base_devices}</b>\n"
f"• Трафик: <b>{base_traffic_text}</b>\n"
f"• Цена: <b>{base_price}₽</b>\n"
"</blockquote>\n"
)
config_block = f"<blockquote>\n{duration_line}\n{devices_line}\n{traffic_line}\n</blockquote>\n"
device_step_line = (
f"💰 Устройства, базовый шаг: {device_step}₽ за каждое устройство сверх базового лимита ({base_devices})"
)
if device_overrides:
parts = []
for k, v in sorted(device_overrides.items(), key=lambda x: int(x[0])):
devices_count = int(k)
extra = int(v)
if devices_count == 0:
label = "безлимит устройств"
else:
label = f"{devices_count} устройств"
parts.append(f"при {label}: индивидуальная доплата {extra}")
device_over_line = "📊 Устройства, индивидуальные доплаты:\n" + "\n".join(f"{p}" for p in parts)
else:
device_over_line = "📊 Устройства, индивидуальные доплаты: не заданы"
device_block = f"<blockquote>{device_step_line}\n{device_over_line}\n</blockquote>\n"
traffic_step_line = f"💰 Трафик, базовый шаг: {traffic_step}₽ за 1 ГБ сверх базового лимита ({base_traffic_text})"
if traffic_overrides:
parts = []
for k, v in sorted(traffic_overrides.items(), key=lambda x: int(x[0])):
gb_value = int(k)
extra = int(v)
if gb_value == 0:
label = "безлимитный трафик"
else:
label = f"лимит {gb_value} ГБ"
parts.append(f"при {label}: индивидуальная доплата {extra}")
traffic_over_line = "📊 Трафик, индивидуальные доплаты:\n" + "\n".join(f"{p}" for p in parts)
else:
traffic_over_line = "📊 Трафик, индивидуальные доплаты: не заданы"
traffic_block = f"<blockquote>\n{traffic_step_line}\n{traffic_over_line}\n</blockquote>"
return (
f"<b>⚙️ Конфигуратор тарифа: {tariff.name}</b>\n\n"
f"Статус: <b>{configurable_text}</b>\n\n"
f"{base_block}"
f"{config_block}"
f"{device_block}"
f"{traffic_block}"
)
@router.callback_query(F.data.startswith("edit_config|"), IsAdminFilter())
async def open_config_menu(callback: CallbackQuery, state: FSMContext, session: AsyncSession):
tariff_id = int(callback.data.split("|")[1])
result = await session.execute(select(Tariff).where(Tariff.id == tariff_id))
tariff = result.scalar_one_or_none()
if not tariff:
await callback.message.edit_text("❌ Тариф не найден.")
return
await state.set_state(TariffConfigState.choosing_section)
await state.update_data(tariff_id=tariff_id)
text = build_config_summary_text(tariff)
await callback.message.edit_text(text=text, reply_markup=build_config_menu_kb(tariff_id))