diff --git a/app/database/crud/subscription.py b/app/database/crud/subscription.py index cd8b5378..91b79375 100644 --- a/app/database/crud/subscription.py +++ b/app/database/crud/subscription.py @@ -1,7 +1,7 @@ import logging from datetime import datetime, timedelta from typing import Optional, List, Tuple -from sqlalchemy import select, and_, func, or_ +from sqlalchemy import select, and_, func from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload @@ -13,11 +13,7 @@ from app.database.models import ( PromoGroup, ) from app.database.crud.notification import clear_notifications -from app.utils.pricing_utils import ( - calculate_months_from_days, - get_remaining_months, - resolve_addon_discount_percent, -) +from app.utils.pricing_utils import calculate_months_from_days, get_remaining_months from app.config import settings logger = logging.getLogger(__name__) @@ -521,23 +517,6 @@ def _get_discount_percent( return 0 -def _get_addon_discount_percent( - user: Optional[User], - promo_group: Optional[PromoGroup], - category: str, - *, - period_days: Optional[int] = None, -) -> int: - group = promo_group or (getattr(user, "promo_group", None) if user else None) - - return resolve_addon_discount_percent( - user, - group, - category, - period_days=period_days, - ) - - async def calculate_subscription_total_cost( db: AsyncSession, period_days: int, @@ -857,7 +836,7 @@ async def calculate_addon_cost_for_remaining_period( if additional_server_ids is None: additional_server_ids = [] - months_to_pay = max(1, get_remaining_months(subscription.end_date)) + months_to_pay = get_remaining_months(subscription.end_date) period_hint_days = months_to_pay * 30 if months_to_pay > 0 else None total_cost = 0 @@ -868,7 +847,7 @@ async def calculate_addon_cost_for_remaining_period( if additional_traffic_gb > 0: traffic_price_per_month = settings.get_traffic_price(additional_traffic_gb) - traffic_discount_percent = _get_addon_discount_percent( + traffic_discount_percent = _get_discount_percent( user, promo_group, "traffic", @@ -889,7 +868,7 @@ async def calculate_addon_cost_for_remaining_period( if additional_devices > 0: devices_price_per_month = additional_devices * settings.PRICE_PER_DEVICE - devices_discount_percent = _get_addon_discount_percent( + devices_discount_percent = _get_discount_percent( user, promo_group, "devices", @@ -913,19 +892,12 @@ async def calculate_addon_cost_for_remaining_period( for server_id in additional_server_ids: result = await db.execute( select(ServerSquad.price_kopeks, ServerSquad.display_name) - .where( - ServerSquad.id == server_id, - ServerSquad.is_available.is_(True), - or_( - ServerSquad.max_users.is_(None), - ServerSquad.current_users < ServerSquad.max_users, - ), - ) + .where(ServerSquad.id == server_id) ) server_data = result.first() if server_data: server_price_per_month, server_name = server_data - servers_discount_percent = _get_addon_discount_percent( + servers_discount_percent = _get_discount_percent( user, promo_group, "servers", diff --git a/app/handlers/subscription.py b/app/handlers/subscription.py index d08f0b48..97b91d0b 100644 --- a/app/handlers/subscription.py +++ b/app/handlers/subscription.py @@ -93,51 +93,6 @@ def _apply_discount_to_monthly_component( } -def _get_addon_discount_percent_for_user( - user: User, - category: str, - period_days: Optional[int] = None, -) -> int: - promo_group = getattr(user, "promo_group", None) - - if promo_group is not None and not getattr(promo_group, "apply_discounts_to_addons", True): - return 0 - - try: - return user.get_promo_discount(category, period_days) - except AttributeError: - return 0 - - -def _calculate_discounted_addon_price( - subscription: Subscription, - user: User, - base_price_per_month: int, - category: str, -) -> Dict[str, int]: - months_to_pay = max(1, get_remaining_months(subscription.end_date)) - period_hint_days = months_to_pay * 30 if months_to_pay > 0 else None - discount_percent = _get_addon_discount_percent_for_user( - user, - category, - period_hint_days, - ) - - discount_per_month = base_price_per_month * discount_percent // 100 - discounted_per_month = base_price_per_month - discount_per_month - total_price = discounted_per_month * months_to_pay - total_discount = discount_per_month * months_to_pay - - return { - "total_price": total_price, - "charged_months": months_to_pay, - "discount_percent": discount_percent, - "discount_total": total_discount, - "discount_per_month": discount_per_month, - "discounted_per_month": discounted_per_month, - } - - async def _prepare_subscription_summary( db_user: User, data: Dict[str, Any], @@ -1332,64 +1287,35 @@ async def apply_countries_changes( logger.info(f"🔧 Добавлено: {added}, Удалено: {removed}") - months_to_pay = max(1, get_remaining_months(subscription.end_date)) - period_hint_days = months_to_pay * 30 if months_to_pay > 0 else None - servers_discount_percent = _get_addon_discount_percent_for_user( - db_user, - "servers", - period_hint_days, - ) - + months_to_pay = get_remaining_months(subscription.end_date) + cost_per_month = 0 added_names = [] removed_names = [] - + added_server_prices = [] - total_cost = 0 - total_discount = 0 - + for country in countries: if country['uuid'] in added: server_price_per_month = country['price_kopeks'] cost_per_month += server_price_per_month added_names.append(country['name']) - server_discount_per_month = ( - server_price_per_month * servers_discount_percent // 100 - ) - discounted_per_month = server_price_per_month - server_discount_per_month - server_total_price = discounted_per_month * months_to_pay - added_server_prices.append(server_total_price) - total_cost += server_total_price - total_discount += server_discount_per_month * months_to_pay if country['uuid'] in removed: removed_names.append(country['name']) - - charged_months = months_to_pay - - if added and servers_discount_percent > 0: - logger.info( - "Стоимость новых серверов: %s₽/мес × %s мес = %s₽ (скидка %s%%: -%s₽)", - cost_per_month / 100, - charged_months, - total_cost / 100, - servers_discount_percent, - total_discount / 100, - ) - else: - logger.info( - "Стоимость новых серверов: %s₽/мес × %s мес = %s₽", - cost_per_month / 100, - charged_months, - total_cost / 100, - ) - + + total_cost, charged_months = calculate_prorated_price(cost_per_month, subscription.end_date) + + for country in countries: + if country['uuid'] in added: + server_price_per_month = country['price_kopeks'] + server_total_price = server_price_per_month * charged_months + added_server_prices.append(server_total_price) + + logger.info(f"Стоимость новых серверов: {cost_per_month/100}₽/мес × {charged_months} мес = {total_cost/100}₽") + if total_cost > 0 and db_user.balance_kopeks < total_cost: missing_kopeks = total_cost - db_user.balance_kopeks required_text = f"{texts.format_price(total_cost)} (за {charged_months} мес)" - if total_discount > 0: - required_text += ( - f"\n💸 Скидка {servers_discount_percent}%: -{texts.format_price(total_discount)}" - ) message_text = texts.t( "ADDON_INSUFFICIENT_FUNDS_MESSAGE", ( @@ -1472,10 +1398,6 @@ async def apply_countries_changes( success_text += "\n".join(f"• {name}" for name in added_names) if total_cost > 0: success_text += f"\n💰 Списано: {texts.format_price(total_cost)} (за {charged_months} мес)" - if total_discount > 0: - success_text += ( - f"\n💸 Скидка {servers_discount_percent}%: -{texts.format_price(total_discount)}" - ) success_text += "\n" if removed_names: @@ -1571,6 +1493,8 @@ async def confirm_change_devices( db_user: User, db: AsyncSession ): + from app.utils.pricing_utils import get_remaining_months, calculate_prorated_price + new_devices_count = int(callback.data.split('_')[2]) texts = get_texts(db_user.language) subscription = db_user.subscription @@ -1598,26 +1522,13 @@ async def confirm_change_devices( chargeable_devices = max(0, additional_devices - free_devices) else: chargeable_devices = additional_devices - + devices_price_per_month = chargeable_devices * settings.PRICE_PER_DEVICE - pricing = _calculate_discounted_addon_price( - subscription, - db_user, - devices_price_per_month, - "devices", - ) - price = pricing["total_price"] - charged_months = pricing["charged_months"] - discount_percent = pricing["discount_percent"] - discount_total = pricing["discount_total"] - + price, charged_months = calculate_prorated_price(devices_price_per_month, subscription.end_date) + if price > 0 and db_user.balance_kopeks < price: missing_kopeks = price - db_user.balance_kopeks required_text = f"{texts.format_price(price)} (за {charged_months} мес)" - if discount_total > 0: - required_text += ( - f"\n💸 Скидка {discount_percent}%: -{texts.format_price(discount_total)}" - ) message_text = texts.t( "ADDON_INSUFFICIENT_FUNDS_MESSAGE", ( @@ -1643,13 +1554,9 @@ async def confirm_change_devices( ) await callback.answer() return - + action_text = f"увеличить до {new_devices_count}" cost_text = f"Доплата: {texts.format_price(price)} (за {charged_months} мес)" if price > 0 else "Бесплатно" - if price > 0 and discount_total > 0: - cost_text += ( - f" (скидка {discount_percent}%: -{texts.format_price(discount_total)})" - ) else: price = 0 @@ -1698,7 +1605,7 @@ async def execute_change_devices( await callback.answer("⚠️ Ошибка списания средств", show_alert=True) return - charged_months = max(1, get_remaining_months(subscription.end_date)) + charged_months = get_remaining_months(subscription.end_date) await create_transaction( db=db, user_id=db_user.id, @@ -2213,6 +2120,8 @@ async def confirm_add_devices( db_user: User, db: AsyncSession ): + from app.utils.pricing_utils import get_remaining_months, calculate_prorated_price + devices_count = int(callback.data.split('_')[2]) texts = get_texts(db_user.language) subscription = db_user.subscription @@ -2230,43 +2139,13 @@ async def confirm_add_devices( return devices_price_per_month = devices_count * settings.PRICE_PER_DEVICE - pricing = _calculate_discounted_addon_price( - subscription, - db_user, - devices_price_per_month, - "devices", - ) - price = pricing["total_price"] - charged_months = pricing["charged_months"] - discount_percent = pricing["discount_percent"] - discount_total = pricing["discount_total"] - - if discount_percent > 0: - logger.info( - "Добавление %s устройств: %s₽/мес × %s мес = %s₽ (скидка %s%%: -%s₽)", - devices_count, - devices_price_per_month / 100, - charged_months, - price / 100, - discount_percent, - discount_total / 100, - ) - else: - logger.info( - "Добавление %s устройств: %s₽/мес × %s мес = %s₽", - devices_count, - devices_price_per_month / 100, - charged_months, - price / 100, - ) + price, charged_months = calculate_prorated_price(devices_price_per_month, subscription.end_date) + + logger.info(f"Добавление {devices_count} устройств: {devices_price_per_month/100}₽/мес × {charged_months} мес = {price/100}₽") if db_user.balance_kopeks < price: missing_kopeks = price - db_user.balance_kopeks required_text = f"{texts.format_price(price)} (за {charged_months} мес)" - if discount_total > 0: - required_text += ( - f"\n💸 Скидка {discount_percent}%: -{texts.format_price(discount_total)}" - ) message_text = texts.t( "ADDON_INSUFFICIENT_FUNDS_MESSAGE", ( @@ -2325,12 +2204,7 @@ async def confirm_add_devices( f"✅ Устройства успешно добавлены!\n\n" f"📱 Добавлено: {devices_count} устройств\n" f"Новый лимит: {subscription.device_limit} устройств\n" - f"💰 Списано: {texts.format_price(price)} (за {charged_months} мес)" - + ( - f"\n💸 Скидка {discount_percent}%: -{texts.format_price(discount_total)}" - if discount_total > 0 - else "" - ), + f"💰 Списано: {texts.format_price(price)} (за {charged_months} мес)", reply_markup=get_back_keyboard(db_user.language) ) @@ -3627,42 +3501,12 @@ async def add_traffic( texts = get_texts(db_user.language) subscription = db_user.subscription - price_per_month = settings.get_traffic_price(traffic_gb) - - if price_per_month == 0 and traffic_gb != 0: + price = settings.get_traffic_price(traffic_gb) + + if price == 0 and traffic_gb != 0: await callback.answer("⚠️ Цена для этого пакета не настроена", show_alert=True) return - - pricing = _calculate_discounted_addon_price( - subscription, - db_user, - price_per_month, - "traffic", - ) - price = pricing["total_price"] - charged_months = pricing["charged_months"] - discount_percent = pricing["discount_percent"] - discount_total = pricing["discount_total"] - - if discount_percent > 0: - logger.info( - "Добавление трафика +%s ГБ: %s₽/мес × %s мес = %s₽ (скидка %s%%: -%s₽)", - traffic_gb, - price_per_month / 100, - charged_months, - price / 100, - discount_percent, - discount_total / 100, - ) - else: - logger.info( - "Добавление трафика +%s ГБ: %s₽/мес × %s мес = %s₽", - traffic_gb, - price_per_month / 100, - charged_months, - price / 100, - ) - + if db_user.balance_kopeks < price: missing_kopeks = price - db_user.balance_kopeks message_text = texts.t( @@ -3675,14 +3519,7 @@ async def add_traffic( "Выберите способ пополнения. Сумма подставится автоматически." ), ).format( - required=( - f"{texts.format_price(price)} (за {charged_months} мес)" - + ( - f"\n💸 Скидка {discount_percent}%: -{texts.format_price(discount_total)}" - if discount_total > 0 - else "" - ) - ), + required=texts.format_price(price), balance=texts.format_price(db_user.balance_kopeks), missing=texts.format_price(missing_kopeks), ) @@ -3701,7 +3538,7 @@ async def add_traffic( try: success = await subtract_user_balance( db, db_user, price, - f"Добавление {traffic_gb} ГБ трафика на {charged_months} мес" + f"Добавление {traffic_gb} ГБ трафика" ) if not success: @@ -3721,9 +3558,7 @@ async def add_traffic( user_id=db_user.id, type=TransactionType.SUBSCRIPTION_PAYMENT, amount_kopeks=price, - description=( - f"Добавление {traffic_gb} ГБ трафика на {charged_months} мес" - ) + description=f"Добавление {traffic_gb} ГБ трафика" ) @@ -3736,12 +3571,6 @@ async def add_traffic( else: success_text += f"📈 Добавлено: {traffic_gb} ГБ\n" success_text += f"Новый лимит: {texts.format_traffic(subscription.traffic_limit_gb)}" - if price > 0: - success_text += f"\n💰 Списано: {texts.format_price(price)} (за {charged_months} мес)" - if discount_total > 0: - success_text += ( - f"\n💸 Скидка {discount_percent}%: -{texts.format_price(discount_total)}" - ) await callback.message.edit_text( success_text, diff --git a/app/services/subscription_service.py b/app/services/subscription_service.py index 4749b11a..54ba6720 100644 --- a/app/services/subscription_service.py +++ b/app/services/subscription_service.py @@ -14,8 +14,7 @@ from app.utils.pricing_utils import ( calculate_months_from_days, get_remaining_months, calculate_prorated_price, - validate_pricing_calculation, - resolve_addon_discount_percent, + validate_pricing_calculation ) logger = logging.getLogger(__name__) @@ -49,9 +48,12 @@ def _resolve_addon_discount_percent( ) -> int: group = promo_group or (getattr(user, "promo_group", None) if user else None) - return resolve_addon_discount_percent( + if group is not None and not getattr(group, "apply_discounts_to_addons", True): + return 0 + + return _resolve_discount_percent( user, - group, + promo_group, category, period_days=period_days, ) diff --git a/app/utils/pricing_utils.py b/app/utils/pricing_utils.py index ead9ddd4..40d7f589 100644 --- a/app/utils/pricing_utils.py +++ b/app/utils/pricing_utils.py @@ -1,14 +1,10 @@ from datetime import datetime, timedelta -from typing import Tuple, Optional, TYPE_CHECKING +from typing import Tuple import logging logger = logging.getLogger(__name__) -if TYPE_CHECKING: # pragma: no cover - from app.database.models import User, PromoGroup - - def calculate_months_from_days(days: int) -> int: return max(1, round(days / 30)) @@ -65,32 +61,6 @@ def apply_percentage_discount(amount: int, percent: int) -> Tuple[int, int]: return discounted_amount, discount_value -def resolve_addon_discount_percent( - user: Optional["User"], - promo_group: Optional["PromoGroup"], - category: str, - *, - period_days: Optional[int] = None, -) -> int: - """Return discount percent for add-on purchases respecting promo-group rules.""" - - group = promo_group or (getattr(user, "promo_group", None) if user else None) - - if group is not None and not getattr(group, "apply_discounts_to_addons", True): - return 0 - - if user is not None: - try: - return user.get_promo_discount(category, period_days) - except AttributeError: - pass - - if promo_group is not None: - return promo_group.get_discount_percent(category, period_days) - - return 0 - - def format_period_description(days: int, language: str = "ru") -> str: months = calculate_months_from_days(days)