recalculation during purchase

This commit is contained in:
Vladless
2025-12-15 23:46:03 +03:00
parent e71a20cd3e
commit 2025329e5f
5 changed files with 137 additions and 60 deletions
+1
View File
@@ -10,6 +10,7 @@ from database.models import Setting
TARIFFS_CONFIG: dict[str, Any] = {
"ALLOW_DOWNGRADE": True,
"KEY_ADDONS_PACK_MODE": "all",
"KEY_ADDONS_PRICE_BASE_MODE": "current",
}
@@ -32,6 +32,7 @@ def build_tariffs_settings_kb() -> InlineKeyboardMarkup:
"""Клавиатура основного экрана настроек тарификации."""
allow_downgrade = bool(TARIFFS_CONFIG.get("ALLOW_DOWNGRADE", True))
pack_mode = TARIFFS_CONFIG.get("KEY_ADDONS_PACK_MODE") or ""
recalc_enabled = bool(TARIFFS_CONFIG.get("KEY_ADDONS_RECALC_PRICE", False))
builder = InlineKeyboardBuilder()
@@ -49,6 +50,13 @@ def build_tariffs_settings_kb() -> InlineKeyboardMarkup:
)
)
builder.row(
InlineKeyboardButton(
text=f"Перерасчёт при докупке: {'да' if recalc_enabled else 'нет'}",
callback_data=AdminPanelCallback(action="settings_tariffs_toggle_addons_recalc").pack(),
)
)
builder.row(
InlineKeyboardButton(
text="Назад",
@@ -63,12 +71,14 @@ def build_tariffs_settings_text() -> str:
"""Текст основного экрана настроек тарификации."""
allow_downgrade = bool(TARIFFS_CONFIG.get("ALLOW_DOWNGRADE", True))
pack_mode = TARIFFS_CONFIG.get("KEY_ADDONS_PACK_MODE") or ""
recalc_enabled = bool(TARIFFS_CONFIG.get("KEY_ADDONS_RECALC_PRICE", False))
lines: list[str] = [
"⚙️ Настройки тарификации",
"",
f"• Понижение условий при изменении: {'включено' if allow_downgrade else 'выключено'}",
f"• Режим доплат пакетами: {format_pack_mode_label(pack_mode)}",
f"• Перерасчёт при докупке: {'да' if recalc_enabled else 'нет'}",
"",
"Нажмите «Режим пакетов», чтобы подробнее настроить доплаты к активной подписке.",
]
@@ -174,6 +184,18 @@ async def toggle_tariffs_downgrade(callback: CallbackQuery, session: AsyncSessio
await refresh_tariffs_settings_screen(callback)
@router.callback_query(AdminPanelCallback.filter(F.action == "settings_tariffs_toggle_addons_recalc"))
async def toggle_tariffs_addons_recalc(callback: CallbackQuery, session: AsyncSession) -> None:
"""Переключает перерасчёт при докупке."""
current = bool(TARIFFS_CONFIG.get("KEY_ADDONS_RECALC_PRICE", False))
new_config: dict[str, Any] = dict(TARIFFS_CONFIG)
new_config["KEY_ADDONS_RECALC_PRICE"] = not current
await update_tariffs_config(session, new_config)
await refresh_tariffs_settings_screen(callback)
@router.callback_query(AdminPanelCallback.filter(F.action == "settings_tariffs_packs"))
async def open_tariffs_packs(callback: CallbackQuery, session: AsyncSession) -> None:
"""Открывает экран выбора режима пакетов."""
+65 -59
View File
@@ -1,4 +1,5 @@
from math import ceil
from typing import Any
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
@@ -7,6 +8,7 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from sqlalchemy.ext.asyncio import AsyncSession
from config import USE_NEW_PAYMENT_FLOW
from core.bootstrap import MODES_CONFIG
from core.settings.tariffs_config import TARIFFS_CONFIG, normalize_tariff_config
from database import (
get_balance,
@@ -36,11 +38,11 @@ from ..buy.key_tariffs import calculate_config_price
from .utils import (
KeyAddonConfigState,
build_addons_pack_screen_text,
calc_remaining_ratio_seconds,
format_devices_label,
format_traffic_label,
)
router = Router()
@@ -74,6 +76,7 @@ async def render_addons_screen(callback: CallbackQuery, state: FSMContext, sessi
current_traffic_gb = data.get("addon_current_traffic_gb")
selected_devices = data.get("addon_selected_device_limit")
selected_traffic_gb = data.get("addon_selected_traffic_gb")
expiry_time = data.get("addon_expiry_time")
if not email or not tariff_id:
logger.warning(f"[ADDONS] PACK_MODE: нет email или tariff_id в состоянии: {data}")
@@ -154,6 +157,10 @@ async def render_addons_screen(callback: CallbackQuery, state: FSMContext, sessi
selected_device_limit=current_devices_for_price,
selected_traffic_gb=current_traffic_for_price,
)
try:
base_price_for_current_int = int(base_price_for_current) if base_price_for_current is not None else 0
except (TypeError, ValueError):
base_price_for_current_int = 0
new_devices_for_price = current_devices_for_price
if has_device_option and selected_devices is not None:
@@ -176,11 +183,29 @@ async def render_addons_screen(callback: CallbackQuery, state: FSMContext, sessi
selected_device_limit=new_devices_for_price,
selected_traffic_gb=new_traffic_for_price,
)
extra_price = max(0, price_with_pack - base_price_for_current)
try:
price_with_pack_int = int(price_with_pack) if price_with_pack is not None else 0
except (TypeError, ValueError):
price_with_pack_int = 0
recalc_enabled = bool(
MODES_CONFIG.get(
"KEY_ADDONS_RECALC_PRICE",
TARIFFS_CONFIG.get("KEY_ADDONS_RECALC_PRICE", False),
)
)
if recalc_enabled:
diff_full = max(0, price_with_pack_int - base_price_for_current_int)
remaining_seconds, total_seconds = calc_remaining_ratio_seconds(expiry_time, tariff)
extra_price = int((diff_full * remaining_seconds + total_seconds - 1) // total_seconds)
else:
extra_price = max(0, price_with_pack_int - base_price_for_current_int)
logger.debug(
"[ADDONS] PACK_MODE calculated prices: "
f"base_price_for_current={base_price_for_current} price_with_pack={price_with_pack} extra_price={extra_price} "
f"base_price_for_current={base_price_for_current_int} price_with_pack={price_with_pack_int} "
f"extra_price={extra_price} recalc_enabled={recalc_enabled} "
f"has_device_option={has_device_option} has_traffic_option={has_traffic_option} pack_mode={pack_mode!r}"
)
@@ -195,23 +220,13 @@ async def render_addons_screen(callback: CallbackQuery, state: FSMContext, sessi
has_device_pack_selected = has_device_option and selected_devices is not None
has_traffic_pack_selected = has_traffic_option and selected_traffic_gb is not None
if has_device_pack_selected:
selected_devices_label = format_devices_label(selected_devices)
else:
selected_devices_label = None
if has_traffic_pack_selected:
selected_traffic_label = format_traffic_label(selected_traffic_gb)
else:
selected_traffic_label = None
selected_devices_label = format_devices_label(selected_devices) if has_device_pack_selected else None
selected_traffic_label = format_traffic_label(selected_traffic_gb) if has_traffic_pack_selected else None
if has_device_pack_selected:
current_devices_value = int(current_devices) if current_devices else 0
selected_devices_value = int(selected_devices)
if current_devices_value <= 0 or selected_devices_value <= 0:
total_devices_value = 0
else:
total_devices_value = current_devices_value + selected_devices_value
total_devices_value = 0 if current_devices_value <= 0 or selected_devices_value <= 0 else current_devices_value + selected_devices_value
total_devices_label = format_devices_label(total_devices_value)
else:
total_devices_label = None
@@ -219,10 +234,7 @@ async def render_addons_screen(callback: CallbackQuery, state: FSMContext, sessi
if has_traffic_pack_selected:
current_traffic_value = int(current_traffic_gb) if current_traffic_gb else 0
selected_traffic_value = int(selected_traffic_gb)
if current_traffic_value <= 0 or selected_traffic_value <= 0:
total_after_gb = 0
else:
total_after_gb = current_traffic_value + selected_traffic_value
total_after_gb = 0 if current_traffic_value <= 0 or selected_traffic_value <= 0 else current_traffic_value + selected_traffic_value
total_traffic_label = format_traffic_label(total_after_gb)
else:
total_traffic_label = None
@@ -295,7 +307,6 @@ async def render_addons_screen(callback: CallbackQuery, state: FSMContext, sessi
elif traffic_buttons:
for button in traffic_buttons:
builder.row(button)
builder.row(
InlineKeyboardButton(
text=CONFIRM_ADDON_BUTTON_TEXT.format(amount=extra_price_text),
@@ -426,7 +437,6 @@ async def start_key_addons(callback: CallbackQuery, state: FSMContext, session:
selected_device_limit_db = record.get("selected_device_limit")
selected_traffic_limit_db = record.get("selected_traffic_limit")
original_price_db = record.get("selected_price_rub")
current_device_limit_db = record.get("current_device_limit")
current_traffic_limit_db = record.get("current_traffic_limit")
@@ -434,7 +444,7 @@ async def start_key_addons(callback: CallbackQuery, state: FSMContext, session:
base_devices = int(base_devices) if base_devices is not None else None
base_traffic_bytes = tariff.get("traffic_limit")
base_traffic_gb = int(base_traffic_bytes / GB) if base_traffic_bytes else None
base_traffic_gb_value = int(base_traffic_bytes / GB) if base_traffic_bytes else None
current_devices = (
int(current_device_limit_db)
@@ -444,7 +454,7 @@ async def start_key_addons(callback: CallbackQuery, state: FSMContext, session:
current_traffic_gb = (
int(current_traffic_limit_db)
if current_traffic_limit_db is not None
else (int(selected_traffic_limit_db) if selected_traffic_limit_db is not None else base_traffic_gb)
else (int(selected_traffic_limit_db) if selected_traffic_limit_db is not None else base_traffic_gb_value)
)
pack_devices, pack_traffic, pack_mode = get_pack_flags()
@@ -456,8 +466,7 @@ async def start_key_addons(callback: CallbackQuery, state: FSMContext, session:
pack_traffic and bool(traffic_options) and not (current_traffic_gb is not None and int(current_traffic_gb) == 0)
)
has_any_pack = has_device_pack or has_traffic_pack
if not has_any_pack:
if not (has_device_pack or has_traffic_pack):
logger.warning(
f"[ADDONS] PACK_MODE: пакеты недоступны, уже максимальные параметры "
f"email={email} current_devices={current_devices} current_traffic_gb={current_traffic_gb} "
@@ -466,30 +475,6 @@ async def start_key_addons(callback: CallbackQuery, state: FSMContext, session:
await callback.message.answer("❌ Для этой подписки пакеты уже недоступны.")
return
current_devices_for_price = int(current_devices) if current_devices is not None else None
current_traffic_gb_for_price = int(current_traffic_gb) if current_traffic_gb is not None else None
config_price_for_current = calculate_config_price(
tariff=tariff,
selected_device_limit=current_devices_for_price,
selected_traffic_gb=current_traffic_gb_for_price,
)
try:
original_price_from_db = int(original_price_db) if original_price_db is not None else 0
except (TypeError, ValueError):
original_price_from_db = 0
original_price = 0
logger.debug(
"[ADDONS] PACK_MODE start_key_addons state: "
f"email={email} tariff_id={tariff_id} current_devices={current_devices} current_traffic_gb={current_traffic_gb} "
f"config_price_for_current={config_price_for_current} original_price_db={original_price_db} "
f"original_price={original_price} original_price_from_db={original_price_from_db} "
f"pack_mode={pack_mode!r}"
)
cfg_for_state = dict(cfg)
if device_options:
cfg_for_state["device_options"] = device_options
@@ -502,7 +487,7 @@ async def start_key_addons(callback: CallbackQuery, state: FSMContext, session:
addon_tariff_config=cfg_for_state,
addon_current_device_limit=current_devices,
addon_current_traffic_gb=current_traffic_gb,
addon_original_price=original_price,
addon_expiry_time=record.get("expiry_time"),
addon_selected_device_limit=None,
addon_selected_traffic_gb=None,
)
@@ -572,7 +557,6 @@ async def handle_addons_confirm(callback: CallbackQuery, state: FSMContext, sess
email = data.get("addon_key_email")
tariff_id = data.get("addon_tariff_id")
original_price = int(data.get("addon_original_price") or 0)
selected_devices = data.get("addon_selected_device_limit")
selected_traffic_gb = data.get("addon_selected_traffic_gb")
current_devices = data.get("addon_current_device_limit")
@@ -580,7 +564,7 @@ async def handle_addons_confirm(callback: CallbackQuery, state: FSMContext, sess
logger.info(
"[ADDONS] PACK_MODE handle_addons_confirm: "
f"tg_id={tg_id} email={email} tariff_id={tariff_id} original_price={original_price} "
f"tg_id={tg_id} email={email} tariff_id={tariff_id} "
f"selected_devices={selected_devices} selected_traffic_gb={selected_traffic_gb} "
f"current_devices={current_devices} current_traffic_gb={current_traffic_gb}"
)
@@ -629,6 +613,10 @@ async def handle_addons_confirm(callback: CallbackQuery, state: FSMContext, sess
selected_device_limit=current_devices_for_price,
selected_traffic_gb=current_traffic_for_price,
)
try:
base_price_for_current_int = int(base_price_for_current) if base_price_for_current is not None else 0
except (TypeError, ValueError):
base_price_for_current_int = 0
new_devices_for_price = current_devices_for_price
if has_device_option and selected_devices is not None:
@@ -651,12 +639,31 @@ async def handle_addons_confirm(callback: CallbackQuery, state: FSMContext, sess
selected_device_limit=new_devices_for_price,
selected_traffic_gb=new_traffic_for_price,
)
extra_price = max(0, price_with_pack - base_price_for_current)
try:
price_with_pack_int = int(price_with_pack) if price_with_pack is not None else 0
except (TypeError, ValueError):
price_with_pack_int = 0
recalc_enabled = bool(
MODES_CONFIG.get(
"KEY_ADDONS_RECALC_PRICE",
TARIFFS_CONFIG.get("KEY_ADDONS_RECALC_PRICE", False),
)
)
if recalc_enabled:
diff_full = max(0, price_with_pack_int - base_price_for_current_int)
remaining_seconds, total_seconds = calc_remaining_ratio_seconds(record.get("expiry_time"), tariff)
extra_price = int((diff_full * remaining_seconds + total_seconds - 1) // total_seconds)
total_price_after_purchase = base_price_for_current_int + extra_price
else:
extra_price = max(0, price_with_pack_int - base_price_for_current_int)
total_price_after_purchase = price_with_pack_int
logger.debug(
"[ADDONS] PACK_MODE confirm prices: "
f"base_price_for_current={base_price_for_current} price_with_pack={price_with_pack} "
f"original_price={original_price} extra_price={extra_price} "
f"base_price_for_current={base_price_for_current_int} price_with_pack={price_with_pack_int} "
f"extra_price={extra_price} recalc_enabled={recalc_enabled} total_price_after_purchase={total_price_after_purchase} "
f"has_device_option={has_device_option} has_traffic_option={has_traffic_option} "
f"pack_mode={pack_mode!r}"
)
@@ -692,7 +699,6 @@ async def handle_addons_confirm(callback: CallbackQuery, state: FSMContext, sess
temp_payload={
"email": email,
"tariff_id": int(tariff_id),
"original_price": original_price,
"selected_device_limit": selected_devices,
"selected_traffic_gb": selected_traffic_gb,
"current_device_limit": current_devices,
@@ -793,7 +799,7 @@ async def handle_addons_confirm(callback: CallbackQuery, state: FSMContext, sess
email=email,
selected_devices=new_device_limit_effective,
selected_traffic_gb=new_traffic_limit_gb_effective,
total_price=int(price_with_pack),
total_price=int(total_price_after_purchase),
has_device_choice=has_device_option,
has_traffic_choice=has_traffic_option,
config_mode="pack",
@@ -806,7 +812,7 @@ async def handle_addons_confirm(callback: CallbackQuery, state: FSMContext, sess
f"tg_id={tg_id} email={email} extra_price={extra_price} "
f"new_device_limit_effective={new_device_limit_effective} "
f"new_traffic_limit_gb_effective={new_traffic_limit_gb_effective} "
f"pack_mode={pack_mode!r}"
f"recalc_enabled={recalc_enabled} pack_mode={pack_mode!r}"
)
await state.clear()
+48
View File
@@ -1,3 +1,6 @@
from datetime import datetime, timezone
from typing import Any
from aiogram.fsm.state import State, StatesGroup
from handlers.texts import (
@@ -58,6 +61,51 @@ def is_not_downgrade(current_value, new_value) -> bool:
return new_cmp >= current_cmp
def calc_remaining_ratio_seconds(expiry_time: Any, tariff: dict) -> tuple[int, int]:
"""Секунды до конца подписки и длительность периода."""
duration_days = int(tariff.get("duration_days") or 0) or 30
total_seconds = max(1, duration_days * 86400)
if not expiry_time:
return total_seconds, total_seconds
expiry_dt: datetime | None = None
if isinstance(expiry_time, datetime):
expiry_dt = expiry_time
elif isinstance(expiry_time, (int, float)):
ts = float(expiry_time)
if ts > 10_000_000_000:
ts = ts / 1000.0
try:
expiry_dt = datetime.fromtimestamp(ts, tz=timezone.utc)
except Exception:
expiry_dt = None
elif isinstance(expiry_time, str):
try:
expiry_dt = datetime.fromisoformat(expiry_time.replace("Z", "+00:00"))
except Exception:
expiry_dt = None
if expiry_dt is None:
return total_seconds, total_seconds
now_utc = datetime.now(timezone.utc)
if expiry_dt.tzinfo is None:
expiry_utc = expiry_dt.replace(tzinfo=timezone.utc)
else:
expiry_utc = expiry_dt.astimezone(timezone.utc)
remaining_seconds = int((expiry_utc - now_utc).total_seconds())
if remaining_seconds <= 0:
return 0, total_seconds
if remaining_seconds > total_seconds:
remaining_seconds = total_seconds
return remaining_seconds, total_seconds
def build_addons_screen_text(
*,
tariff_name: str,
+1 -1
View File
@@ -92,4 +92,4 @@ def get_git_commit_number() -> str:
def get_version() -> str:
return f"v.5.1-a141229 {get_git_commit_number()}"
return f"v.5.1-a151229 {get_git_commit_number()}"