278 lines
8.5 KiB
Python
278 lines
8.5 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from math import ceil
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from core.bootstrap import TARIFFS_CONFIG
|
|
from core.settings.tariffs_config import normalize_tariff_config
|
|
from database import get_balance, get_key_details, get_tariff_by_id, save_key_config_with_mode
|
|
from database.coupons import mark_coupon_used
|
|
from database.users import update_balance
|
|
from logger import logger
|
|
|
|
from .errors import InsufficientFundsError, NotFoundError, ValidationError
|
|
from .keys import resolve_cluster_name
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
def get_pack_flags() -> tuple[bool, bool, str]:
|
|
"""Определяет доступные опции pack mode из конфига."""
|
|
mode = TARIFFS_CONFIG.get("KEY_ADDONS_PACK_MODE") or ""
|
|
if not mode:
|
|
return False, False, ""
|
|
if mode == "traffic":
|
|
return False, True, mode
|
|
if mode == "devices":
|
|
return True, False, mode
|
|
if mode == "all":
|
|
return True, True, mode
|
|
return False, False, mode
|
|
|
|
|
|
def get_override_value(overrides: Any, key: int) -> Any:
|
|
if not isinstance(overrides, dict):
|
|
return None
|
|
if key in overrides:
|
|
return overrides.get(key)
|
|
return overrides.get(str(key))
|
|
|
|
|
|
def calc_pack_devices_price_rub(tariff: dict[str, Any], pack_devices: int | None) -> int:
|
|
if pack_devices is None:
|
|
return 0
|
|
pack_devices = int(pack_devices)
|
|
overrides = tariff.get("device_overrides") or {}
|
|
if pack_devices == 0:
|
|
override = get_override_value(overrides, 0)
|
|
return int(ceil(float(override))) if override is not None else 0
|
|
if pack_devices < 0:
|
|
return 0
|
|
override = get_override_value(overrides, pack_devices)
|
|
if override is not None:
|
|
return int(ceil(float(override)))
|
|
step_price = int(tariff.get("device_step_rub") or 0)
|
|
return int(ceil(pack_devices * step_price))
|
|
|
|
|
|
def calc_pack_traffic_price_rub(tariff: dict[str, Any], pack_traffic_gb: int | None) -> int:
|
|
if pack_traffic_gb is None:
|
|
return 0
|
|
pack_traffic_gb = int(pack_traffic_gb)
|
|
overrides = tariff.get("traffic_overrides") or {}
|
|
if pack_traffic_gb == 0:
|
|
override = get_override_value(overrides, 0)
|
|
return int(ceil(float(override))) if override is not None else 0
|
|
if pack_traffic_gb < 0:
|
|
return 0
|
|
override = get_override_value(overrides, pack_traffic_gb)
|
|
if override is not None:
|
|
return int(ceil(float(override)))
|
|
step_price = int(tariff.get("traffic_step_rub") or 0)
|
|
return int(ceil(pack_traffic_gb * step_price))
|
|
|
|
|
|
def calc_pack_full_price_rub(
|
|
tariff: dict[str, Any],
|
|
has_device_option: bool,
|
|
has_traffic_option: bool,
|
|
selected_devices: int | None,
|
|
selected_traffic_gb: int | None,
|
|
) -> int:
|
|
total = 0
|
|
if has_device_option:
|
|
total += calc_pack_devices_price_rub(tariff, selected_devices)
|
|
if has_traffic_option:
|
|
total += calc_pack_traffic_price_rub(tariff, selected_traffic_gb)
|
|
return int(total)
|
|
|
|
|
|
@dataclass
|
|
class AddonsPreviewResult:
|
|
"""Результат предпросмотра аддонов."""
|
|
|
|
total_price_rub: int
|
|
extra_price_rub: int
|
|
balance: float
|
|
required_amount: int
|
|
payment_required: bool
|
|
current_device_limit: int | None
|
|
current_traffic_gb: int | None
|
|
selected_device_limit: int | None
|
|
selected_traffic_gb: int | None
|
|
has_device_option: bool
|
|
has_traffic_option: bool
|
|
|
|
|
|
@dataclass
|
|
class AddonsApplyResult:
|
|
"""Результат применения аддонов."""
|
|
|
|
ok: bool
|
|
client_id: str
|
|
tariff_id: int
|
|
total_price_rub: int
|
|
extra_price_rub: int
|
|
charged_rub: int
|
|
balance_rub: float
|
|
|
|
|
|
async def preview_addons(
|
|
session: AsyncSession,
|
|
billing_user_id: int,
|
|
client_id: str,
|
|
key_email: str,
|
|
tariff_id: int,
|
|
selected_device_limit: int | None = None,
|
|
selected_traffic_gb: int | None = None,
|
|
) -> AddonsPreviewResult:
|
|
"""Считает стоимость аддонов без применения.
|
|
|
|
Raises: NotFoundError, ValidationError
|
|
"""
|
|
tariff = await get_tariff_by_id(session, tariff_id)
|
|
if not tariff:
|
|
raise NotFoundError("Тариф не найден")
|
|
|
|
normalize_tariff_config(tariff)
|
|
has_device, has_traffic, _ = get_pack_flags()
|
|
|
|
key_details = await get_key_details(session, key_email)
|
|
if not key_details:
|
|
raise NotFoundError("Подписка не найдена")
|
|
|
|
current_device = key_details.get("current_device_limit")
|
|
current_traffic = key_details.get("current_traffic_limit")
|
|
|
|
total_price = calc_pack_full_price_rub(
|
|
tariff,
|
|
has_device,
|
|
has_traffic,
|
|
selected_device_limit,
|
|
selected_traffic_gb,
|
|
)
|
|
|
|
current_price = calc_pack_full_price_rub(
|
|
tariff,
|
|
has_device,
|
|
has_traffic,
|
|
current_device,
|
|
current_traffic,
|
|
)
|
|
extra_price = max(0, total_price - current_price)
|
|
|
|
balance = float(await get_balance(session, billing_user_id))
|
|
required = max(0, int(ceil(float(extra_price) - balance)))
|
|
|
|
return AddonsPreviewResult(
|
|
total_price_rub=total_price,
|
|
extra_price_rub=extra_price,
|
|
balance=balance,
|
|
required_amount=required,
|
|
payment_required=required > 0,
|
|
current_device_limit=current_device,
|
|
current_traffic_gb=current_traffic,
|
|
selected_device_limit=selected_device_limit,
|
|
selected_traffic_gb=selected_traffic_gb,
|
|
has_device_option=has_device,
|
|
has_traffic_option=has_traffic,
|
|
)
|
|
|
|
|
|
async def apply_addons(
|
|
session: AsyncSession,
|
|
billing_user_id: int,
|
|
client_id: str,
|
|
key_email: str,
|
|
key_server_id: str,
|
|
tariff_id: int,
|
|
extra_price_rub: int,
|
|
selected_device_limit: int | None = None,
|
|
selected_traffic_gb: int | None = None,
|
|
coupon_id: int | None = None,
|
|
) -> AddonsApplyResult:
|
|
"""Применяет аддоны к ключу: обновляет лимиты на кластере и в БД.
|
|
|
|
Raises: NotFoundError, InsufficientFundsError
|
|
"""
|
|
tariff = await get_tariff_by_id(session, tariff_id)
|
|
if not tariff:
|
|
raise NotFoundError("Тариф не найден")
|
|
|
|
key_details = await get_key_details(session, key_email)
|
|
if not key_details:
|
|
raise NotFoundError("Подписка не найдена")
|
|
|
|
from services.operations import renew_key_in_cluster
|
|
from services.tariffs.tariff_display import GB, get_effective_limits_for_key
|
|
|
|
dev_eff, trf_bytes = await get_effective_limits_for_key(
|
|
session=session,
|
|
tariff_id=tariff_id,
|
|
selected_device_limit=selected_device_limit,
|
|
selected_traffic_gb=selected_traffic_gb if selected_traffic_gb is not None else 0,
|
|
)
|
|
total_gb = int(trf_bytes / GB) if trf_bytes else 0
|
|
|
|
cluster_id = await resolve_cluster_name(session, key_server_id)
|
|
if not cluster_id:
|
|
raise NotFoundError(f"Кластер для {key_server_id} не найден")
|
|
|
|
expiry_ms = int(key_details.get("expiry_time") or 0)
|
|
|
|
await renew_key_in_cluster(
|
|
cluster_id=cluster_id,
|
|
email=key_email,
|
|
client_id=client_id,
|
|
new_expiry_time=expiry_ms,
|
|
total_gb=total_gb,
|
|
session=session,
|
|
hwid_device_limit=dev_eff,
|
|
reset_traffic=False,
|
|
target_subgroup=tariff.get("subgroup_title"),
|
|
old_subgroup=tariff.get("subgroup_title"),
|
|
plan=tariff_id,
|
|
)
|
|
|
|
if extra_price_rub > 0:
|
|
await update_balance(session, billing_user_id, -extra_price_rub)
|
|
|
|
normalize_tariff_config(tariff)
|
|
has_device, has_traffic, _ = get_pack_flags()
|
|
total_price = calc_pack_full_price_rub(
|
|
tariff,
|
|
has_device,
|
|
has_traffic,
|
|
selected_device_limit,
|
|
selected_traffic_gb,
|
|
)
|
|
|
|
await save_key_config_with_mode(
|
|
session=session,
|
|
email=key_email,
|
|
selected_devices=selected_device_limit,
|
|
selected_traffic_gb=selected_traffic_gb,
|
|
total_price=total_price,
|
|
has_device_choice=has_device,
|
|
has_traffic_choice=has_traffic,
|
|
config_mode="addons",
|
|
)
|
|
|
|
if coupon_id is not None:
|
|
await mark_coupon_used(session, coupon_id, billing_user_id)
|
|
|
|
new_balance = float(await get_balance(session, billing_user_id))
|
|
|
|
return AddonsApplyResult(
|
|
ok=True,
|
|
client_id=client_id,
|
|
tariff_id=tariff_id,
|
|
total_price_rub=total_price,
|
|
extra_price_rub=extra_price_rub,
|
|
charged_rub=extra_price_rub,
|
|
balance_rub=new_balance,
|
|
)
|