mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-17 01:20:34 +00:00
@@ -2,7 +2,7 @@
|
||||
import html
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict, Any
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
from aiogram import types, F
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
from aiogram.fsm.context import FSMContext
|
||||
@@ -17,6 +17,7 @@ from app.services.subscription_purchase_service import SubscriptionPurchaseServi
|
||||
from app.utils.decorators import error_handler
|
||||
from app.states import SubscriptionStates
|
||||
from app.utils.subscription_utils import get_display_subscription_link
|
||||
from app.utils.pricing_utils import compute_simple_subscription_price
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -49,11 +50,7 @@ async def start_simple_subscription_purchase(
|
||||
|
||||
# Сохраняем параметры в состояние
|
||||
await state.update_data(subscription_params=subscription_params)
|
||||
|
||||
# Проверяем баланс пользователя
|
||||
user_balance_kopeks = getattr(db_user, "balance_kopeks", 0)
|
||||
# Рассчитываем цену подписки
|
||||
price_kopeks = _calculate_simple_subscription_price(subscription_params)
|
||||
|
||||
data = await state.get_data()
|
||||
resolved_squad_uuid = await _ensure_simple_subscription_squad_uuid(
|
||||
db,
|
||||
@@ -62,39 +59,38 @@ async def start_simple_subscription_purchase(
|
||||
user_id=db_user.id,
|
||||
state_data=data,
|
||||
)
|
||||
period_days = subscription_params["period_days"]
|
||||
recorded_price = getattr(settings, f"PRICE_{period_days}_DAYS", price_kopeks)
|
||||
direct_purchase_min_balance = recorded_price
|
||||
extra_components = []
|
||||
traffic_limit = subscription_params.get("traffic_limit_gb", 0)
|
||||
if traffic_limit and traffic_limit > 0:
|
||||
traffic_price = settings.get_traffic_price(traffic_limit)
|
||||
direct_purchase_min_balance += traffic_price
|
||||
extra_components.append(f"traffic={traffic_limit}GB->{traffic_price}")
|
||||
|
||||
device_limit = subscription_params.get("device_limit", 1)
|
||||
if device_limit and device_limit > settings.DEFAULT_DEVICE_LIMIT:
|
||||
additional_devices = device_limit - settings.DEFAULT_DEVICE_LIMIT
|
||||
devices_price = additional_devices * settings.PRICE_PER_DEVICE
|
||||
direct_purchase_min_balance += devices_price
|
||||
extra_components.append(f"devices+{additional_devices}->{devices_price}")
|
||||
logger.warning(
|
||||
"SIMPLE_SUBSCRIPTION_DEBUG_START | user=%s | period=%s | base_price=%s | recorded_price=%s | extras=%s | total=%s | env_PRICE_30=%s",
|
||||
db_user.id,
|
||||
period_days,
|
||||
price_kopeks,
|
||||
recorded_price,
|
||||
",".join(extra_components) if extra_components else "none",
|
||||
direct_purchase_min_balance,
|
||||
getattr(settings, "PRICE_30_DAYS", None),
|
||||
price_kopeks, price_breakdown = await _calculate_simple_subscription_price(
|
||||
db,
|
||||
subscription_params,
|
||||
user=db_user,
|
||||
resolved_squad_uuid=resolved_squad_uuid,
|
||||
)
|
||||
|
||||
can_pay_from_balance = user_balance_kopeks >= direct_purchase_min_balance
|
||||
period_days = subscription_params["period_days"]
|
||||
user_balance_kopeks = getattr(db_user, "balance_kopeks", 0)
|
||||
|
||||
logger.warning(
|
||||
"SIMPLE_SUBSCRIPTION_DEBUG_START | user=%s | period=%s | base=%s | traffic=%s | devices=%s | servers=%s | discount=%s | total=%s | squads=%s",
|
||||
db_user.id,
|
||||
period_days,
|
||||
price_breakdown.get("base_price", 0),
|
||||
price_breakdown.get("traffic_price", 0),
|
||||
price_breakdown.get("devices_price", 0),
|
||||
price_breakdown.get("servers_price", 0),
|
||||
price_breakdown.get("total_discount", 0),
|
||||
price_kopeks,
|
||||
",".join(price_breakdown.get("resolved_squad_uuids", []))
|
||||
if price_breakdown.get("resolved_squad_uuids")
|
||||
else "none",
|
||||
)
|
||||
|
||||
can_pay_from_balance = user_balance_kopeks >= price_kopeks
|
||||
logger.warning(
|
||||
"SIMPLE_SUBSCRIPTION_DEBUG_START_BALANCE | user=%s | balance=%s | min_required=%s | can_pay=%s",
|
||||
db_user.id,
|
||||
user_balance_kopeks,
|
||||
direct_purchase_min_balance,
|
||||
price_kopeks,
|
||||
can_pay_from_balance,
|
||||
)
|
||||
|
||||
@@ -158,27 +154,23 @@ async def start_simple_subscription_purchase(
|
||||
await callback.answer()
|
||||
|
||||
|
||||
def _calculate_simple_subscription_price(params: dict) -> int:
|
||||
async def _calculate_simple_subscription_price(
|
||||
db: AsyncSession,
|
||||
params: dict,
|
||||
*,
|
||||
user: Optional[User] = None,
|
||||
resolved_squad_uuid: Optional[str] = None,
|
||||
) -> Tuple[int, Dict[str, Any]]:
|
||||
"""Рассчитывает цену простой подписки."""
|
||||
period_days = params.get("period_days", 30)
|
||||
attr_name = f"PRICE_{period_days}_DAYS"
|
||||
attr_value = getattr(settings, attr_name, None)
|
||||
|
||||
logger.warning(
|
||||
"SIMPLE_SUBSCRIPTION_DEBUG_PRICE_FUNC | period=%s | attr=%s | attr_value=%s | base_price=%s",
|
||||
period_days,
|
||||
attr_name,
|
||||
attr_value,
|
||||
settings.BASE_SUBSCRIPTION_PRICE,
|
||||
resolved_uuids = [resolved_squad_uuid] if resolved_squad_uuid else None
|
||||
return await compute_simple_subscription_price(
|
||||
db,
|
||||
params,
|
||||
user=user,
|
||||
resolved_squad_uuids=resolved_uuids,
|
||||
)
|
||||
|
||||
# Получаем цену для стандартного периода
|
||||
if attr_value is not None:
|
||||
return attr_value
|
||||
else:
|
||||
# Если нет цены для конкретного периода, используем базовую цену
|
||||
return settings.BASE_SUBSCRIPTION_PRICE
|
||||
|
||||
|
||||
def _get_simple_subscription_payment_keyboard(language: str) -> types.InlineKeyboardMarkup:
|
||||
"""Создает клавиатуру с методами оплаты для простой подписки."""
|
||||
@@ -335,27 +327,22 @@ async def handle_simple_subscription_pay_with_balance(
|
||||
)
|
||||
|
||||
# Рассчитываем цену подписки
|
||||
price_kopeks = _calculate_simple_subscription_price(subscription_params)
|
||||
recorded_price = getattr(settings, f"PRICE_{subscription_params['period_days']}_DAYS", price_kopeks)
|
||||
total_required = recorded_price
|
||||
extras = []
|
||||
traffic_limit = subscription_params.get("traffic_limit_gb", 0)
|
||||
if traffic_limit and traffic_limit > 0:
|
||||
traffic_price = settings.get_traffic_price(traffic_limit)
|
||||
total_required += traffic_price
|
||||
extras.append(f"traffic={traffic_limit}GB->{traffic_price}")
|
||||
device_limit = subscription_params.get("device_limit", 1)
|
||||
if device_limit and device_limit > settings.DEFAULT_DEVICE_LIMIT:
|
||||
additional_devices = device_limit - settings.DEFAULT_DEVICE_LIMIT
|
||||
devices_price = additional_devices * settings.PRICE_PER_DEVICE
|
||||
total_required += devices_price
|
||||
extras.append(f"devices+{additional_devices}->{devices_price}")
|
||||
price_kopeks, price_breakdown = await _calculate_simple_subscription_price(
|
||||
db,
|
||||
subscription_params,
|
||||
user=db_user,
|
||||
resolved_squad_uuid=resolved_squad_uuid,
|
||||
)
|
||||
total_required = price_kopeks
|
||||
logger.warning(
|
||||
"SIMPLE_SUBSCRIPTION_DEBUG_PAY_BALANCE | user=%s | period=%s | base_price=%s | extras=%s | total_required=%s | balance=%s",
|
||||
"SIMPLE_SUBSCRIPTION_DEBUG_PAY_BALANCE | user=%s | period=%s | base=%s | traffic=%s | devices=%s | servers=%s | discount=%s | total_required=%s | balance=%s",
|
||||
db_user.id,
|
||||
subscription_params["period_days"],
|
||||
price_kopeks,
|
||||
",".join(extras) if extras else "none",
|
||||
price_breakdown.get("base_price", 0),
|
||||
price_breakdown.get("traffic_price", 0),
|
||||
price_breakdown.get("devices_price", 0),
|
||||
price_breakdown.get("servers_price", 0),
|
||||
price_breakdown.get("total_discount", 0),
|
||||
total_required,
|
||||
getattr(db_user, "balance_kopeks", 0),
|
||||
)
|
||||
@@ -595,29 +582,38 @@ async def handle_simple_subscription_other_payment_methods(
|
||||
await callback.answer("❌ Данные подписки устарели. Пожалуйста, начните сначала.", show_alert=True)
|
||||
return
|
||||
|
||||
resolved_squad_uuid = await _ensure_simple_subscription_squad_uuid(
|
||||
db,
|
||||
state,
|
||||
subscription_params,
|
||||
user_id=db_user.id,
|
||||
state_data=data,
|
||||
)
|
||||
|
||||
# Рассчитываем цену подписки
|
||||
price_kopeks = _calculate_simple_subscription_price(subscription_params)
|
||||
price_kopeks, price_breakdown = await _calculate_simple_subscription_price(
|
||||
db,
|
||||
subscription_params,
|
||||
user=db_user,
|
||||
resolved_squad_uuid=resolved_squad_uuid,
|
||||
)
|
||||
|
||||
user_balance_kopeks = getattr(db_user, "balance_kopeks", 0)
|
||||
recorded_price = getattr(settings, f"PRICE_{subscription_params['period_days']}_DAYS", price_kopeks)
|
||||
total_required = recorded_price
|
||||
if subscription_params.get("traffic_limit_gb", 0) > 0:
|
||||
total_required += settings.get_traffic_price(subscription_params["traffic_limit_gb"])
|
||||
if subscription_params.get("device_limit", 1) > settings.DEFAULT_DEVICE_LIMIT:
|
||||
additional_devices = subscription_params["device_limit"] - settings.DEFAULT_DEVICE_LIMIT
|
||||
total_required += additional_devices * settings.PRICE_PER_DEVICE
|
||||
can_pay_from_balance = user_balance_kopeks >= total_required
|
||||
can_pay_from_balance = user_balance_kopeks >= price_kopeks
|
||||
logger.warning(
|
||||
"SIMPLE_SUBSCRIPTION_DEBUG_METHODS | user=%s | balance=%s | base_price=%s | total_required=%s | can_pay=%s",
|
||||
"SIMPLE_SUBSCRIPTION_DEBUG_METHODS | user=%s | balance=%s | base=%s | traffic=%s | devices=%s | servers=%s | discount=%s | total_required=%s | can_pay=%s",
|
||||
db_user.id,
|
||||
user_balance_kopeks,
|
||||
price_breakdown.get("base_price", 0),
|
||||
price_breakdown.get("traffic_price", 0),
|
||||
price_breakdown.get("devices_price", 0),
|
||||
price_breakdown.get("servers_price", 0),
|
||||
price_breakdown.get("total_discount", 0),
|
||||
price_kopeks,
|
||||
total_required,
|
||||
can_pay_from_balance,
|
||||
)
|
||||
|
||||
# Отображаем доступные методы оплаты
|
||||
resolved_squad_uuid = data.get("resolved_squad_uuid")
|
||||
server_label = _get_simple_subscription_server_label(
|
||||
texts,
|
||||
subscription_params,
|
||||
@@ -677,11 +673,8 @@ async def handle_simple_subscription_payment_method(
|
||||
await callback.answer("❌ Данные подписки устарели. Пожалуйста, начните сначала.", show_alert=True)
|
||||
return
|
||||
|
||||
# Рассчитываем цену подписки
|
||||
price_kopeks = _calculate_simple_subscription_price(subscription_params)
|
||||
|
||||
payment_method = callback.data.replace("simple_subscription_", "")
|
||||
|
||||
|
||||
try:
|
||||
payment_service = PaymentService(callback.bot)
|
||||
|
||||
@@ -693,6 +686,14 @@ async def handle_simple_subscription_payment_method(
|
||||
state_data=data,
|
||||
)
|
||||
|
||||
# Рассчитываем цену подписки
|
||||
price_kopeks, _ = await _calculate_simple_subscription_price(
|
||||
db,
|
||||
subscription_params,
|
||||
user=db_user,
|
||||
resolved_squad_uuid=resolved_squad_uuid,
|
||||
)
|
||||
|
||||
if payment_method == "stars":
|
||||
# Оплата через Telegram Stars
|
||||
stars_count = settings.rubles_to_stars(settings.kopeks_to_rubles(price_kopeks))
|
||||
|
||||
@@ -64,6 +64,7 @@ from app.states import SubscriptionStates
|
||||
from app.utils.pagination import paginate_list
|
||||
from app.utils.pricing_utils import (
|
||||
calculate_months_from_days,
|
||||
compute_simple_subscription_price,
|
||||
get_remaining_months,
|
||||
calculate_prorated_price,
|
||||
validate_pricing_calculation,
|
||||
@@ -2412,7 +2413,22 @@ async def handle_simple_subscription_purchase(
|
||||
# Проверяем баланс пользователя
|
||||
user_balance_kopeks = getattr(db_user, "balance_kopeks", 0)
|
||||
# Рассчитываем цену подписки
|
||||
price_kopeks = _calculate_simple_subscription_price(subscription_params)
|
||||
price_kopeks, price_breakdown = await _calculate_simple_subscription_price(
|
||||
db,
|
||||
subscription_params,
|
||||
user=db_user,
|
||||
resolved_squad_uuid=subscription_params.get("squad_uuid"),
|
||||
)
|
||||
logger.debug(
|
||||
"SIMPLE_SUBSCRIPTION_PURCHASE_PRICE | user=%s | total=%s | base=%s | traffic=%s | devices=%s | servers=%s | discount=%s",
|
||||
db_user.id,
|
||||
price_kopeks,
|
||||
price_breakdown.get("base_price", 0),
|
||||
price_breakdown.get("traffic_price", 0),
|
||||
price_breakdown.get("devices_price", 0),
|
||||
price_breakdown.get("servers_price", 0),
|
||||
price_breakdown.get("total_discount", 0),
|
||||
)
|
||||
traffic_text = (
|
||||
"Безлимит"
|
||||
if subscription_params["traffic_limit_gb"] == 0
|
||||
@@ -2464,16 +2480,22 @@ async def handle_simple_subscription_purchase(
|
||||
|
||||
|
||||
|
||||
def _calculate_simple_subscription_price(params: dict) -> int:
|
||||
async def _calculate_simple_subscription_price(
|
||||
db: AsyncSession,
|
||||
params: dict,
|
||||
*,
|
||||
user: Optional[User] = None,
|
||||
resolved_squad_uuid: Optional[str] = None,
|
||||
) -> Tuple[int, Dict[str, Any]]:
|
||||
"""Рассчитывает цену простой подписки."""
|
||||
period_days = params.get("period_days", 30)
|
||||
|
||||
# Получаем цену для стандартного периода
|
||||
if hasattr(settings, f'PRICE_{period_days}_DAYS'):
|
||||
return getattr(settings, f'PRICE_{period_days}_DAYS')
|
||||
else:
|
||||
# Если нет цены для конкретного периода, используем базовую цену
|
||||
return settings.BASE_SUBSCRIPTION_PRICE
|
||||
|
||||
resolved_uuids = [resolved_squad_uuid] if resolved_squad_uuid else None
|
||||
return await compute_simple_subscription_price(
|
||||
db,
|
||||
params,
|
||||
user=user,
|
||||
resolved_squad_uuids=resolved_uuids,
|
||||
)
|
||||
|
||||
|
||||
def _get_simple_subscription_payment_keyboard(language: str) -> types.InlineKeyboardMarkup:
|
||||
@@ -2564,7 +2586,22 @@ async def _extend_existing_subscription(
|
||||
"traffic_limit_gb": traffic_limit_gb,
|
||||
"squad_uuid": squad_uuid
|
||||
}
|
||||
price_kopeks = _calculate_simple_subscription_price(subscription_params)
|
||||
price_kopeks, price_breakdown = await _calculate_simple_subscription_price(
|
||||
db,
|
||||
subscription_params,
|
||||
user=db_user,
|
||||
resolved_squad_uuid=squad_uuid,
|
||||
)
|
||||
logger.debug(
|
||||
"SIMPLE_SUBSCRIPTION_EXTEND_PRICE | user=%s | total=%s | base=%s | traffic=%s | devices=%s | servers=%s | discount=%s",
|
||||
db_user.id,
|
||||
price_kopeks,
|
||||
price_breakdown.get("base_price", 0),
|
||||
price_breakdown.get("traffic_price", 0),
|
||||
price_breakdown.get("devices_price", 0),
|
||||
price_breakdown.get("servers_price", 0),
|
||||
price_breakdown.get("total_discount", 0),
|
||||
)
|
||||
|
||||
# Проверяем баланс пользователя
|
||||
if db_user.balance_kopeks < price_kopeks:
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Tuple
|
||||
from typing import Any, Dict, List, Optional, Sequence, Tuple, TYPE_CHECKING
|
||||
import logging
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import settings
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from app.database.models import User, PromoGroup
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -68,6 +75,218 @@ def apply_percentage_discount(amount: int, percent: int) -> Tuple[int, int]:
|
||||
return discounted_amount, discount_value
|
||||
|
||||
|
||||
def resolve_discount_percent(
|
||||
user: Optional["User"],
|
||||
promo_group: Optional["PromoGroup"],
|
||||
category: str,
|
||||
*,
|
||||
period_days: Optional[int] = None,
|
||||
) -> int:
|
||||
"""Определяет размер скидки для указанной категории."""
|
||||
|
||||
if user is not None:
|
||||
try:
|
||||
return user.get_promo_discount(category, period_days)
|
||||
except AttributeError: # pragma: no cover - defensive guard
|
||||
pass
|
||||
|
||||
if promo_group is not None:
|
||||
return promo_group.get_discount_percent(category, period_days)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
async def compute_simple_subscription_price(
|
||||
db: AsyncSession,
|
||||
params: Dict[str, Any],
|
||||
*,
|
||||
user: Optional["User"] = None,
|
||||
resolved_squad_uuids: Optional[Sequence[str]] = None,
|
||||
) -> Tuple[int, Dict[str, Any]]:
|
||||
"""Вычисляет стоимость простой подписки с учетом всех доплат и скидок."""
|
||||
|
||||
period_days = int(params.get("period_days", 30) or 30)
|
||||
attr_name = f"PRICE_{period_days}_DAYS"
|
||||
base_price_original = getattr(settings, attr_name, settings.BASE_SUBSCRIPTION_PRICE)
|
||||
|
||||
traffic_limit_raw = params.get("traffic_limit_gb")
|
||||
try:
|
||||
traffic_limit = int(traffic_limit_raw) if traffic_limit_raw is not None else None
|
||||
except (TypeError, ValueError): # pragma: no cover - defensive conversion
|
||||
traffic_limit = None
|
||||
|
||||
if traffic_limit is None or traffic_limit <= 0:
|
||||
# Default simple subscriptions already include unlimited traffic.
|
||||
traffic_price_original = 0
|
||||
else:
|
||||
traffic_price_original = settings.get_traffic_price(traffic_limit)
|
||||
|
||||
device_limit_raw = params.get("device_limit", settings.DEFAULT_DEVICE_LIMIT)
|
||||
try:
|
||||
device_limit = int(device_limit_raw)
|
||||
except (TypeError, ValueError): # pragma: no cover - defensive conversion
|
||||
device_limit = settings.DEFAULT_DEVICE_LIMIT
|
||||
additional_devices = max(0, device_limit - settings.DEFAULT_DEVICE_LIMIT)
|
||||
devices_price_original = additional_devices * settings.PRICE_PER_DEVICE
|
||||
|
||||
promo_group: Optional["PromoGroup"] = params.get("promo_group")
|
||||
|
||||
if promo_group is None:
|
||||
promo_group_id = params.get("promo_group_id")
|
||||
if promo_group_id:
|
||||
from app.database.crud.promo_group import get_promo_group_by_id
|
||||
|
||||
promo_group = await get_promo_group_by_id(db, int(promo_group_id))
|
||||
|
||||
if promo_group is None and user is not None:
|
||||
promo_group = getattr(user, "promo_group", None)
|
||||
|
||||
period_discount_percent = resolve_discount_percent(
|
||||
user,
|
||||
promo_group,
|
||||
"period",
|
||||
period_days=period_days,
|
||||
)
|
||||
base_discount = base_price_original * period_discount_percent // 100
|
||||
discounted_base = base_price_original - base_discount
|
||||
|
||||
traffic_discount_percent = resolve_discount_percent(
|
||||
user,
|
||||
promo_group,
|
||||
"traffic",
|
||||
period_days=period_days,
|
||||
)
|
||||
traffic_discount = traffic_price_original * traffic_discount_percent // 100
|
||||
discounted_traffic = traffic_price_original - traffic_discount
|
||||
|
||||
devices_discount_percent = resolve_discount_percent(
|
||||
user,
|
||||
promo_group,
|
||||
"devices",
|
||||
period_days=period_days,
|
||||
)
|
||||
devices_discount = devices_price_original * devices_discount_percent // 100
|
||||
discounted_devices = devices_price_original - devices_discount
|
||||
|
||||
servers_discount_percent = resolve_discount_percent(
|
||||
user,
|
||||
promo_group,
|
||||
"servers",
|
||||
period_days=period_days,
|
||||
)
|
||||
|
||||
resolved_uuids: List[str] = []
|
||||
if resolved_squad_uuids:
|
||||
resolved_uuids.extend([uuid for uuid in resolved_squad_uuids if uuid])
|
||||
else:
|
||||
raw_squad = params.get("squad_uuid")
|
||||
if isinstance(raw_squad, (list, tuple, set)):
|
||||
resolved_uuids.extend([str(uuid) for uuid in raw_squad if uuid])
|
||||
elif raw_squad:
|
||||
resolved_uuids.append(str(raw_squad))
|
||||
|
||||
from app.database.crud.server_squad import get_server_squad_by_uuid
|
||||
|
||||
server_breakdown: List[Dict[str, Any]] = []
|
||||
servers_price_original = 0
|
||||
servers_discount_total = 0
|
||||
|
||||
for squad_uuid in resolved_uuids:
|
||||
server = await get_server_squad_by_uuid(db, squad_uuid)
|
||||
if not server:
|
||||
logger.warning(
|
||||
"SIMPLE_SUBSCRIPTION_PRICE_SERVER_NOT_FOUND | squad=%s",
|
||||
squad_uuid,
|
||||
)
|
||||
server_breakdown.append(
|
||||
{
|
||||
"uuid": squad_uuid,
|
||||
"name": None,
|
||||
"available": False,
|
||||
"original_price": 0,
|
||||
"discount": 0,
|
||||
"final_price": 0,
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
if not server.is_available or server.is_full:
|
||||
logger.warning(
|
||||
"SIMPLE_SUBSCRIPTION_PRICE_SERVER_UNAVAILABLE | squad=%s | available=%s | full=%s",
|
||||
squad_uuid,
|
||||
server.is_available,
|
||||
server.is_full,
|
||||
)
|
||||
server_breakdown.append(
|
||||
{
|
||||
"uuid": squad_uuid,
|
||||
"name": server.display_name,
|
||||
"available": False,
|
||||
"original_price": 0,
|
||||
"discount": 0,
|
||||
"final_price": 0,
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
original_price = server.price_kopeks
|
||||
discount_value = original_price * servers_discount_percent // 100
|
||||
final_price = original_price - discount_value
|
||||
|
||||
servers_price_original += original_price
|
||||
servers_discount_total += discount_value
|
||||
|
||||
server_breakdown.append(
|
||||
{
|
||||
"uuid": squad_uuid,
|
||||
"name": server.display_name,
|
||||
"available": True,
|
||||
"original_price": original_price,
|
||||
"discount": discount_value,
|
||||
"final_price": final_price,
|
||||
}
|
||||
)
|
||||
|
||||
total_before_discount = (
|
||||
base_price_original
|
||||
+ traffic_price_original
|
||||
+ devices_price_original
|
||||
+ servers_price_original
|
||||
)
|
||||
|
||||
total_discount = (
|
||||
base_discount
|
||||
+ traffic_discount
|
||||
+ devices_discount
|
||||
+ servers_discount_total
|
||||
)
|
||||
|
||||
total_price = max(0, total_before_discount - total_discount)
|
||||
|
||||
breakdown = {
|
||||
"base_price": base_price_original,
|
||||
"base_discount": base_discount,
|
||||
"traffic_price": traffic_price_original,
|
||||
"traffic_discount": traffic_discount,
|
||||
"devices_price": devices_price_original,
|
||||
"devices_discount": devices_discount,
|
||||
"servers_price": servers_price_original,
|
||||
"servers_discount": servers_discount_total,
|
||||
"servers_final": sum(item["final_price"] for item in server_breakdown),
|
||||
"server_details": server_breakdown,
|
||||
"total_before_discount": total_before_discount,
|
||||
"total_discount": total_discount,
|
||||
"resolved_squad_uuids": resolved_uuids,
|
||||
"applied_promo_group_id": getattr(promo_group, "id", None) if promo_group else None,
|
||||
"period_discount_percent": period_discount_percent,
|
||||
"traffic_discount_percent": traffic_discount_percent,
|
||||
"devices_discount_percent": devices_discount_percent,
|
||||
"servers_discount_percent": servers_discount_percent,
|
||||
}
|
||||
|
||||
return total_price, breakdown
|
||||
|
||||
|
||||
def format_period_description(days: int, language: str = "ru") -> str:
|
||||
months = calculate_months_from_days(days)
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Глобальные фикстуры и настройки окружения для тестов."""
|
||||
|
||||
import asyncio
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
@@ -149,3 +151,50 @@ if "yookassa" not in sys.modules:
|
||||
def fixed_datetime() -> datetime:
|
||||
"""Возвращает фиксированную отметку времени для воспроизводимых проверок."""
|
||||
return datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def pytest_configure(config: pytest.Config) -> None:
|
||||
"""Регистрируем маркеры для асинхронных тестов."""
|
||||
|
||||
config.addinivalue_line(
|
||||
"markers",
|
||||
"asyncio: запуск асинхронного теста через встроенный цикл событий",
|
||||
)
|
||||
config.addinivalue_line(
|
||||
"markers",
|
||||
"anyio: запуск асинхронного теста через встроенный цикл событий",
|
||||
)
|
||||
|
||||
|
||||
def _unwrap_test(obj): # noqa: ANN001 - вспомогательная функция для определения coroutine
|
||||
"""Возвращает исходную функцию, снимая обёртки pytest и декораторов."""
|
||||
|
||||
unwrapped = obj
|
||||
while hasattr(unwrapped, "__wrapped__"):
|
||||
unwrapped = unwrapped.__wrapped__
|
||||
return unwrapped
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_pyfunc_call(pyfuncitem: pytest.Function) -> bool | None:
|
||||
"""Позволяет запускать async def тесты без дополнительных плагинов."""
|
||||
|
||||
test_func = _unwrap_test(pyfuncitem.obj)
|
||||
if not inspect.iscoroutinefunction(test_func):
|
||||
return None
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
try:
|
||||
asyncio.set_event_loop(loop)
|
||||
signature = inspect.signature(test_func)
|
||||
call_kwargs = {
|
||||
name: value
|
||||
for name, value in pyfuncitem.funcargs.items()
|
||||
if name in signature.parameters
|
||||
}
|
||||
loop.run_until_complete(pyfuncitem.obj(**call_kwargs))
|
||||
finally:
|
||||
asyncio.set_event_loop(None)
|
||||
loop.close()
|
||||
|
||||
return True
|
||||
|
||||
@@ -77,7 +77,7 @@ async def test_create_bill_success(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
assert client.calls and client.calls[0]["amount"] == Decimal("500.00")
|
||||
assert client.calls[0]["shop_id"] == "shop42"
|
||||
assert client.calls[0]["description"] == "Пополнение"
|
||||
assert client.calls[0]["custom"] == json.dumps({"extra": "value"}, ensure_ascii=False, separators=(",", ":"))
|
||||
assert client.calls[0]["custom"] == {"extra": "value"}
|
||||
assert client.calls[0]["payment_method"] == "BANK_CARD"
|
||||
|
||||
|
||||
|
||||
@@ -112,6 +112,7 @@ async def test_delete_user_cart_not_found(user_cart_service):
|
||||
|
||||
assert result is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_has_user_cart(user_cart_service, mock_redis):
|
||||
"""Тест проверки наличия корзины пользователя"""
|
||||
|
||||
Reference in New Issue
Block a user