diff --git a/app/config.py b/app/config.py index 7a8c275e..a2cbaf80 100644 --- a/app/config.py +++ b/app/config.py @@ -117,8 +117,7 @@ class Settings(BaseSettings): BASE_PROMO_GROUP_PERIOD_DISCOUNTS_ENABLED: bool = False BASE_PROMO_GROUP_PERIOD_DISCOUNTS: str = "" - TRAFFIC_SELECTION_MODE: str = "selectable" - SUBSCRIPTION_PURCHASE_MODE: str = "custom" + TRAFFIC_SELECTION_MODE: str = "selectable" FIXED_TRAFFIC_LIMIT_GB: int = 100 REFERRAL_MINIMUM_TOPUP_KOPEKS: int = 10000 @@ -528,20 +527,7 @@ class Settings(BaseSettings): def is_traffic_fixed(self) -> bool: return self.TRAFFIC_SELECTION_MODE.lower() == "fixed" - - def get_subscription_purchase_mode(self) -> str: - mode = (self.SUBSCRIPTION_PURCHASE_MODE or "custom").strip().lower() - return mode or "custom" - - def is_subscription_tariff_mode(self) -> bool: - return self.get_subscription_purchase_mode() == "tariff" - - def is_subscription_custom_mode(self) -> bool: - return self.get_subscription_purchase_mode() == "custom" - - def is_subscription_fixed_mode(self) -> bool: - return self.get_subscription_purchase_mode() == "fixed" - + def get_fixed_traffic_limit(self) -> int: return self.FIXED_TRAFFIC_LIMIT_GB diff --git a/app/database/crud/subscription.py b/app/database/crud/subscription.py index 34948855..97a23b3c 100644 --- a/app/database/crud/subscription.py +++ b/app/database/crud/subscription.py @@ -22,13 +22,10 @@ logger = logging.getLogger(__name__) async def get_subscription_by_user_id(db: AsyncSession, user_id: int) -> Optional[Subscription]: result = await db.execute( select(Subscription) - .options( - selectinload(Subscription.user), - selectinload(Subscription.tariff), - ) + .options(selectinload(Subscription.user)) .where(Subscription.user_id == user_id) .order_by(Subscription.created_at.desc()) - .limit(1) + .limit(1) ) subscription = result.scalar_one_or_none() @@ -77,14 +74,13 @@ async def create_paid_subscription( db: AsyncSession, user_id: int, duration_days: int, - traffic_limit_gb: int = 0, + traffic_limit_gb: int = 0, device_limit: int = 1, - connected_squads: List[str] = None, - tariff_id: Optional[int] = None, + connected_squads: List[str] = None ) -> Subscription: - + end_date = datetime.utcnow() + timedelta(days=duration_days) - + subscription = Subscription( user_id=user_id, status=SubscriptionStatus.ACTIVE.value, @@ -93,8 +89,7 @@ async def create_paid_subscription( end_date=end_date, traffic_limit_gb=traffic_limit_gb, device_limit=device_limit, - connected_squads=connected_squads or [], - tariff_id=tariff_id, + connected_squads=connected_squads or [] ) db.add(subscription) @@ -271,10 +266,7 @@ async def get_expiring_subscriptions( result = await db.execute( select(Subscription) - .options( - selectinload(Subscription.user), - selectinload(Subscription.tariff), - ) + .options(selectinload(Subscription.user)) .where( and_( Subscription.status == SubscriptionStatus.ACTIVE.value, @@ -290,10 +282,7 @@ async def get_expired_subscriptions(db: AsyncSession) -> List[Subscription]: result = await db.execute( select(Subscription) - .options( - selectinload(Subscription.user), - selectinload(Subscription.tariff), - ) + .options(selectinload(Subscription.user)) .where( and_( Subscription.status == SubscriptionStatus.ACTIVE.value, @@ -309,15 +298,12 @@ async def get_subscriptions_for_autopay(db: AsyncSession) -> List[Subscription]: result = await db.execute( select(Subscription) - .options( - selectinload(Subscription.user), - selectinload(Subscription.tariff), - ) + .options(selectinload(Subscription.user)) .where( and_( Subscription.status == SubscriptionStatus.ACTIVE.value, Subscription.autopay_enabled == True, - Subscription.is_trial == False + Subscription.is_trial == False ) ) ) @@ -463,10 +449,7 @@ async def get_all_subscriptions( result = await db.execute( select(Subscription) - .options( - selectinload(Subscription.user), - selectinload(Subscription.tariff), - ) + .options(selectinload(Subscription.user)) .order_by(Subscription.created_at.desc()) .offset(offset) .limit(limit) @@ -762,7 +745,6 @@ async def get_subscription_renewal_cost( select(Subscription) .options( selectinload(Subscription.user).selectinload(User.promo_group), - selectinload(Subscription.tariff), ) .where(Subscription.id == subscription_id) ) diff --git a/app/database/crud/tariff.py b/app/database/crud/tariff.py deleted file mode 100644 index d28b1207..00000000 --- a/app/database/crud/tariff.py +++ /dev/null @@ -1,266 +0,0 @@ -import logging -from typing import Iterable, List, Optional, Sequence - -from sqlalchemy import delete, func, select -from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy.orm import selectinload - -from app.database.models import ( - PromoGroup, - ServerSquad, - Subscription, - SubscriptionTariff, - SubscriptionTariffPrice, -) - -logger = logging.getLogger(__name__) - - -async def list_tariffs( - db: AsyncSession, - *, - include_inactive: bool = False, -) -> List[SubscriptionTariff]: - query = ( - select(SubscriptionTariff) - .options( - selectinload(SubscriptionTariff.promo_groups), - selectinload(SubscriptionTariff.server_squads), - selectinload(SubscriptionTariff.prices), - ) - .order_by(SubscriptionTariff.sort_order, SubscriptionTariff.name) - ) - - if not include_inactive: - query = query.where(SubscriptionTariff.is_active.is_(True)) - - result = await db.execute(query) - return result.scalars().unique().all() - - -async def get_tariff_by_id( - db: AsyncSession, - tariff_id: int, - *, - include_inactive: bool = False, -) -> Optional[SubscriptionTariff]: - query = ( - select(SubscriptionTariff) - .options( - selectinload(SubscriptionTariff.promo_groups), - selectinload(SubscriptionTariff.server_squads), - selectinload(SubscriptionTariff.prices), - ) - .where(SubscriptionTariff.id == tariff_id) - ) - - if not include_inactive: - query = query.where(SubscriptionTariff.is_active.is_(True)) - - result = await db.execute(query) - return result.scalars().unique().one_or_none() - - -async def _resolve_servers( - db: AsyncSession, - server_uuids: Sequence[str], -) -> List[ServerSquad]: - if not server_uuids: - return [] - - seen = set() - normalized: List[str] = [] - for raw_uuid in server_uuids: - if not raw_uuid: - continue - cleaned = raw_uuid.strip() - if not cleaned or cleaned in seen: - continue - seen.add(cleaned) - normalized.append(cleaned) - if not normalized: - return [] - - result = await db.execute( - select(ServerSquad) - .options(selectinload(ServerSquad.allowed_promo_groups)) - .where(ServerSquad.squad_uuid.in_(normalized)) - ) - servers = result.scalars().unique().all() - - missing = set(normalized) - {server.squad_uuid for server in servers} - if missing: - logger.warning("Не найдены серверы для тарифов: %s", ", ".join(sorted(missing))) - - ordered_servers = sorted( - servers, - key=lambda server: normalized.index(server.squad_uuid) if server.squad_uuid in normalized else len(normalized), - ) - return ordered_servers - - -async def _resolve_promo_groups( - db: AsyncSession, - promo_group_ids: Optional[Iterable[int]], -) -> List[PromoGroup]: - if promo_group_ids is None: - return [] - - normalized = [int(pg_id) for pg_id in {int(pg_id) for pg_id in promo_group_ids}] - if not normalized: - return [] - - result = await db.execute(select(PromoGroup).where(PromoGroup.id.in_(normalized))) - promo_groups = result.scalars().all() - - missing = set(normalized) - {group.id for group in promo_groups} - if missing: - logger.warning("Не найдены промогруппы для тарифов: %s", ", ".join(map(str, sorted(missing)))) - - return promo_groups - - -def _normalize_prices(prices: Iterable[dict]) -> List[SubscriptionTariffPrice]: - unique_periods = {} - for price in prices or []: - try: - period = int(price.get('period_days')) - amount = int(price.get('price_kopeks')) - except (TypeError, ValueError): - continue - - if period <= 0 or amount < 0: - continue - - unique_periods[period] = amount - - normalized = [ - SubscriptionTariffPrice(period_days=period, price_kopeks=amount) - for period, amount in sorted(unique_periods.items()) - ] - return normalized - - -async def create_tariff( - db: AsyncSession, - *, - name: str, - description: Optional[str] = None, - traffic_limit_gb: int = 0, - device_limit: int = 1, - server_uuids: Sequence[str] = (), - promo_group_ids: Optional[Iterable[int]] = None, - prices: Iterable[dict] = (), - is_active: bool = True, - sort_order: int = 0, -) -> SubscriptionTariff: - servers = await _resolve_servers(db, server_uuids) - promo_groups = await _resolve_promo_groups(db, promo_group_ids) - price_models = _normalize_prices(prices) - - tariff = SubscriptionTariff( - name=name.strip(), - description=description, - traffic_limit_gb=max(0, int(traffic_limit_gb or 0)), - device_limit=max(1, int(device_limit or 1)), - is_active=bool(is_active), - sort_order=int(sort_order or 0), - server_squads=servers, - promo_groups=promo_groups, - prices=price_models, - ) - - db.add(tariff) - await db.commit() - await db.refresh(tariff) - - logger.info("Создан тариф '%s' (ID: %s)", tariff.name, tariff.id) - return tariff - - -async def update_tariff( - db: AsyncSession, - tariff_id: int, - *, - name: Optional[str] = None, - description: Optional[str] = None, - traffic_limit_gb: Optional[int] = None, - device_limit: Optional[int] = None, - server_uuids: Optional[Sequence[str]] = None, - promo_group_ids: Optional[Iterable[int]] = None, - prices: Optional[Iterable[dict]] = None, - is_active: Optional[bool] = None, - sort_order: Optional[int] = None, -) -> Optional[SubscriptionTariff]: - tariff = await get_tariff_by_id(db, tariff_id, include_inactive=True) - if not tariff: - return None - - if name is not None: - tariff.name = name.strip() - if description is not None: - tariff.description = description - if traffic_limit_gb is not None: - tariff.traffic_limit_gb = max(0, int(traffic_limit_gb)) - if device_limit is not None: - tariff.device_limit = max(1, int(device_limit)) - if is_active is not None: - tariff.is_active = bool(is_active) - if sort_order is not None: - tariff.sort_order = int(sort_order) - - if server_uuids is not None: - tariff.server_squads = await _resolve_servers(db, server_uuids) - if promo_group_ids is not None: - tariff.promo_groups = await _resolve_promo_groups(db, promo_group_ids) - if prices is not None: - tariff.prices = _normalize_prices(prices) - - await db.commit() - await db.refresh(tariff) - logger.info("Обновлен тариф '%s' (ID: %s)", tariff.name, tariff.id) - return tariff - - -async def delete_tariff(db: AsyncSession, tariff_id: int) -> bool: - result = await db.execute( - select(func.count(Subscription.id)).where(Subscription.tariff_id == tariff_id) - ) - active_subscriptions = result.scalar() or 0 - if active_subscriptions > 0: - logger.warning( - "Нельзя удалить тариф %s: %s подписок использует его", - tariff_id, - active_subscriptions, - ) - return False - - await db.execute( - delete(SubscriptionTariff).where(SubscriptionTariff.id == tariff_id) - ) - await db.commit() - logger.info("Удален тариф ID %s", tariff_id) - return True - - -async def get_active_tariffs_for_promo_group( - db: AsyncSession, - promo_group_id: Optional[int], -) -> List[SubscriptionTariff]: - tariffs = await list_tariffs(db, include_inactive=False) - result = [] - for tariff in tariffs: - if not tariff.is_available_for_promo_group(promo_group_id): - continue - if not tariff.server_squads: - continue - available_servers = [ - server - for server in tariff.server_squads - if server.is_available and not server.is_full - ] - if not available_servers: - continue - tariff.server_squads = available_servers - result.append(tariff) - return result diff --git a/app/database/models.py b/app/database/models.py index c88962e3..26c4f860 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -43,42 +43,6 @@ server_squad_promo_groups = Table( ) -subscription_tariff_promo_groups = Table( - "subscription_tariff_promo_groups", - Base.metadata, - Column( - "tariff_id", - Integer, - ForeignKey("subscription_tariffs.id", ondelete="CASCADE"), - primary_key=True, - ), - Column( - "promo_group_id", - Integer, - ForeignKey("promo_groups.id", ondelete="CASCADE"), - primary_key=True, - ), -) - - -subscription_tariff_server_squads = Table( - "subscription_tariff_server_squads", - Base.metadata, - Column( - "tariff_id", - Integer, - ForeignKey("subscription_tariffs.id", ondelete="CASCADE"), - primary_key=True, - ), - Column( - "server_squad_id", - Integer, - ForeignKey("server_squads.id", ondelete="CASCADE"), - primary_key=True, - ), -) - - class UserStatus(Enum): ACTIVE = "active" BLOCKED = "blocked" @@ -341,13 +305,6 @@ class PromoGroup(Base): lazy="selectin", ) - tariffs = relationship( - "SubscriptionTariff", - secondary=subscription_tariff_promo_groups, - back_populates="promo_groups", - lazy="selectin", - ) - def _get_period_discounts_map(self) -> Dict[int, int]: raw_discounts = self.period_discounts or {} @@ -483,14 +440,12 @@ class Subscription(Base): subscription_crypto_link = Column(String, nullable=True) device_limit = Column(Integer, default=1) - + connected_squads = Column(JSON, default=list) - - tariff_id = Column(Integer, ForeignKey("subscription_tariffs.id", ondelete="SET NULL"), nullable=True) - + autopay_enabled = Column(Boolean, default=False) autopay_days_before = Column(Integer, default=3) - + created_at = Column(DateTime, default=func.now()) updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) @@ -498,7 +453,6 @@ class Subscription(Base): user = relationship("User", back_populates="subscription") discount_offers = relationship("DiscountOffer", back_populates="subscription") - tariff = relationship("SubscriptionTariff", back_populates="subscriptions") @property def is_active(self) -> bool: @@ -916,13 +870,6 @@ class ServerSquad(Base): back_populates="server_squads", lazy="selectin", ) - - tariffs = relationship( - "SubscriptionTariff", - secondary=subscription_tariff_server_squads, - back_populates="server_squads", - lazy="selectin", - ) @property def price_rubles(self) -> float: @@ -944,75 +891,9 @@ class ServerSquad(Base): return "Доступен" -class SubscriptionTariff(Base): - __tablename__ = "subscription_tariffs" - - id = Column(Integer, primary_key=True, index=True) - name = Column(String(255), unique=True, nullable=False) - description = Column(Text, nullable=True) - traffic_limit_gb = Column(Integer, nullable=False, default=0) - device_limit = Column(Integer, nullable=False, default=1) - is_active = Column(Boolean, nullable=False, default=True) - sort_order = Column(Integer, nullable=False, default=0) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) - - promo_groups = relationship( - "PromoGroup", - secondary=subscription_tariff_promo_groups, - back_populates="tariffs", - lazy="selectin", - ) - server_squads = relationship( - "ServerSquad", - secondary=subscription_tariff_server_squads, - back_populates="tariffs", - lazy="selectin", - ) - prices = relationship( - "SubscriptionTariffPrice", - back_populates="tariff", - cascade="all, delete-orphan", - order_by="SubscriptionTariffPrice.period_days", - ) - subscriptions = relationship("Subscription", back_populates="tariff") - - def is_available_for_promo_group(self, promo_group_id: Optional[int]) -> bool: - if not self.promo_groups: - return True - if promo_group_id is None: - return False - return any(pg.id == promo_group_id for pg in self.promo_groups if pg is not None) - - def get_price_for_period(self, period_days: int) -> Optional[int]: - for price in self.prices: - if price.period_days == period_days: - return price.price_kopeks - return None - - def get_server_uuids(self) -> List[str]: - return [server.squad_uuid for server in self.server_squads if server and server.squad_uuid] - - -class SubscriptionTariffPrice(Base): - __tablename__ = "subscription_tariff_prices" - __table_args__ = ( - UniqueConstraint("tariff_id", "period_days", name="uq_tariff_period_price"), - ) - - id = Column(Integer, primary_key=True, index=True) - tariff_id = Column(Integer, ForeignKey("subscription_tariffs.id", ondelete="CASCADE"), nullable=False) - period_days = Column(Integer, nullable=False) - price_kopeks = Column(Integer, nullable=False) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) - - tariff = relationship("SubscriptionTariff", back_populates="prices") - - class SubscriptionServer(Base): __tablename__ = "subscription_servers" - + id = Column(Integer, primary_key=True, index=True) subscription_id = Column(Integer, ForeignKey("subscriptions.id"), nullable=False) server_squad_id = Column(Integer, ForeignKey("server_squads.id"), nullable=False) diff --git a/app/handlers/subscription.py b/app/handlers/subscription.py index cdbd944c..52800062 100644 --- a/app/handlers/subscription.py +++ b/app/handlers/subscription.py @@ -40,9 +40,7 @@ from app.keyboards.inline import ( get_happ_download_button_row, get_payment_methods_keyboard_with_cart, get_subscription_confirm_keyboard_with_cart, - get_insufficient_balance_keyboard_with_cart, - get_tariff_selection_keyboard, - get_tariff_period_keyboard, + get_insufficient_balance_keyboard_with_cart ) from app.localization.texts import get_texts from app.services.admin_notification_service import AdminNotificationService @@ -54,7 +52,6 @@ from app.services.subscription_checkout_service import ( should_offer_checkout_resume, ) from app.services.subscription_service import SubscriptionService -from app.services.tariff_service import TariffService from app.states import SubscriptionStates from app.utils.pagination import paginate_list from app.utils.pricing_utils import ( @@ -444,68 +441,6 @@ def _build_subscription_period_prompt(db_user: User, texts) -> str: return f"{base_text}\n\n{promo_text}\n" -def _build_tariff_selection_prompt(texts) -> str: - return texts.t( - "TARIFF_SELECTION_PROMPT", - ( - "Выберите тариф\n\n" - "Доступные тарифы ниже зависят от вашей промогруппы и доступных серверов." - ), - ) - - -def _build_tariff_details_text(tariff, texts, language: str) -> str: - description = tariff.description or texts.t("TARIFF_NO_DESCRIPTION", "Описание отсутствует") - server_names = [server.display_name for server in tariff.server_squads] - servers_text = "\n".join(f"• {name}" for name in server_names) if server_names else texts.t( - "TARIFF_NO_SERVERS", "Нет доступных серверов" - ) - traffic_text = texts.format_traffic(tariff.traffic_limit_gb) - devices_text = texts.t("TARIFF_DEVICE_LIMIT", "{count} устройств").format(count=tariff.device_limit) - - return texts.t( - "TARIFF_DETAILS_TEMPLATE", - ( - "{name}\n\n" - "{description}\n\n" - "🌐 Серверы:\n{servers}\n\n" - "📊 Трафик: {traffic}\n" - "📱 Устройства: {devices}\n\n" - "Выберите срок действия тарифа:"" - ), - ).format( - name=tariff.name, - description=description, - servers=servers_text, - traffic=traffic_text, - devices=devices_text, - ) - - -def _build_tariff_summary_text(tariff, period_days: int, price_kopeks: int, texts, language: str) -> str: - period_text = format_period_description(period_days, language) - traffic_text = texts.format_traffic(tariff.traffic_limit_gb) - devices_text = texts.t("TARIFF_DEVICE_LIMIT", "{count} устройств").format(count=tariff.device_limit) - - return texts.t( - "TARIFF_CONFIRMATION_TEMPLATE", - ( - "{name}\n\n" - "📅 Период: {period}\n" - "📊 Трафик: {traffic}\n" - "📱 Устройства: {devices}\n" - "💰 Стоимость: {price}\n\n" - "Подтвердить покупку тарифа?" - ), - ).format( - name=tariff.name, - period=period_text, - traffic=traffic_text, - devices=devices_text, - price=texts.format_price(price_kopeks), - ) - - async def show_subscription_info( callback: types.CallbackQuery, db_user: User, @@ -514,17 +449,6 @@ async def show_subscription_info( await db.refresh(db_user) texts = get_texts(db_user.language) - - if settings.is_subscription_tariff_mode(): - await callback.answer( - texts.t( - "TARIFF_DEVICE_MANAGEMENT_DISABLED", - "ℹ️ Изменение количества устройств недоступно при использовании тарифов", - ), - show_alert=True, - ) - return - subscription = db_user.subscription if not subscription: @@ -1108,38 +1032,10 @@ async def activate_trial( async def start_subscription_purchase( callback: types.CallbackQuery, state: FSMContext, - db_user: User, - db: AsyncSession + db_user: User ): texts = get_texts(db_user.language) - if settings.is_subscription_tariff_mode(): - service = TariffService() - tariffs = await service.get_available_tariffs(db, db_user) - - await state.clear() - - if not tariffs: - await callback.message.edit_text( - texts.t( - "NO_TARIFFS_AVAILABLE", - "❌ Доступных тарифов не найдено. Пожалуйста, обратитесь в поддержку.", - ), - reply_markup=get_back_keyboard(db_user.language), - parse_mode="HTML", - ) - else: - await state.set_state(SubscriptionStates.selecting_tariff) - await state.update_data({'tariff_mode': True}) - await callback.message.edit_text( - _build_tariff_selection_prompt(texts), - reply_markup=get_tariff_selection_keyboard(tariffs, db_user.language), - parse_mode="HTML", - ) - - await callback.answer() - return - await callback.message.edit_text( _build_subscription_period_prompt(db_user, texts), reply_markup=get_subscription_period_keyboard(db_user.language) @@ -1168,187 +1064,6 @@ async def start_subscription_purchase( await callback.answer() -async def handle_tariff_selection( - callback: types.CallbackQuery, - state: FSMContext, - db_user: User, - db: AsyncSession -): - if not settings.is_subscription_tariff_mode(): - await callback.answer() - return - - try: - tariff_id = int(callback.data.split('_')[2]) - except (IndexError, ValueError): - await callback.answer("❌ Некорректный тариф", show_alert=True) - return - - texts = get_texts(db_user.language) - service = TariffService() - tariff = await service.get_tariff_for_user(db, tariff_id, db_user) - - if not tariff or not getattr(tariff, "prices", None): - await callback.answer( - texts.t("TARIFF_UNAVAILABLE", "⚠️ Этот тариф сейчас недоступен"), - show_alert=True, - ) - return - - await state.update_data({ - 'tariff_mode': True, - 'selected_tariff_id': tariff.id, - }) - - await state.set_state(SubscriptionStates.selecting_tariff_period) - await callback.message.edit_text( - _build_tariff_details_text(tariff, texts, db_user.language), - reply_markup=get_tariff_period_keyboard(tariff, db_user.language), - parse_mode="HTML", - ) - await callback.answer() - - -async def handle_tariff_back( - callback: types.CallbackQuery, - state: FSMContext, - db_user: User, - db: AsyncSession -): - if not settings.is_subscription_tariff_mode(): - await callback.answer() - return - - texts = get_texts(db_user.language) - service = TariffService() - tariffs = await service.get_available_tariffs(db, db_user) - - if not tariffs: - await callback.message.edit_text( - texts.t( - "NO_TARIFFS_AVAILABLE", - "❌ Доступных тарифов не найдено. Пожалуйста, обратитесь в поддержку.", - ), - reply_markup=get_back_keyboard(db_user.language), - parse_mode="HTML", - ) - await state.clear() - await callback.answer() - return - - await state.set_state(SubscriptionStates.selecting_tariff) - await state.update_data({'tariff_mode': True}) - await callback.message.edit_text( - _build_tariff_selection_prompt(texts), - reply_markup=get_tariff_selection_keyboard(tariffs, db_user.language), - parse_mode="HTML", - ) - await callback.answer() - - -async def handle_tariff_period_selection( - callback: types.CallbackQuery, - state: FSMContext, - db_user: User, - db: AsyncSession -): - if not settings.is_subscription_tariff_mode(): - await callback.answer() - return - - try: - _, _, tariff_id_str, period_str = callback.data.split('_') - tariff_id = int(tariff_id_str) - period_days = int(period_str) - except (ValueError, IndexError): - await callback.answer("❌ Некорректный срок тарифа", show_alert=True) - return - - texts = get_texts(db_user.language) - service = TariffService() - tariff = await service.get_tariff_for_user(db, tariff_id, db_user) - - if not tariff: - await callback.answer( - texts.t("TARIFF_UNAVAILABLE", "⚠️ Этот тариф сейчас недоступен"), - show_alert=True, - ) - return - - price_kopeks = tariff.get_price_for_period(period_days) - if price_kopeks is None: - await callback.answer( - texts.t("TARIFF_PERIOD_UNAVAILABLE", "⚠️ Для этого тарифа нет указанного периода"), - show_alert=True, - ) - return - - server_uuids = tariff.get_server_uuids() - summary_text = _build_tariff_summary_text(tariff, period_days, price_kopeks, texts, db_user.language) - - months_in_period = calculate_months_from_days(period_days) - - state_payload = { - 'tariff_mode': True, - 'tariff_id': tariff.id, - 'tariff_name': tariff.name, - 'period_days': period_days, - 'total_price': price_kopeks, - 'base_price': price_kopeks, - 'base_price_original': price_kopeks, - 'base_discount_percent': 0, - 'base_discount_total': 0, - 'traffic_gb': tariff.traffic_limit_gb, - 'traffic_price_per_month': 0, - 'traffic_discount_percent': 0, - 'traffic_discount_total': 0, - 'traffic_discounted_price_per_month': 0, - 'total_traffic_price': 0, - 'devices': tariff.device_limit, - 'devices_price_per_month': 0, - 'devices_discount_percent': 0, - 'devices_discount_total': 0, - 'devices_discounted_price_per_month': 0, - 'total_devices_price': 0, - 'countries': server_uuids, - 'servers_price_per_month': 0, - 'servers_discount_percent': 0, - 'servers_discount_total': 0, - 'servers_discounted_price_per_month': 0, - 'server_prices_for_period': [0 for _ in server_uuids], - 'total_servers_price': 0, - 'discounted_monthly_additions': 0, - 'months_in_period': months_in_period, - } - - await state.set_data(state_payload) - await state.set_state(SubscriptionStates.confirming_purchase) - - await callback.message.edit_text( - summary_text, - reply_markup=get_subscription_confirm_keyboard(db_user.language), - parse_mode="HTML", - ) - await callback.answer() - - -async def handle_change_tariff( - callback: types.CallbackQuery, - state: FSMContext, - db_user: User, - db: AsyncSession -): - if not settings.is_subscription_tariff_mode(): - texts = get_texts(db_user.language) - await callback.answer( - texts.t("TARIFF_MODE_DISABLED", "ℹ️ Смена тарифа недоступна в текущем режиме"), - show_alert=True, - ) - return - - await start_subscription_purchase(callback, state, db_user, db) - - async def save_cart_and_redirect_to_topup( callback: types.CallbackQuery, state: FSMContext, @@ -1451,14 +1166,6 @@ async def handle_add_countries( db: AsyncSession, state: FSMContext ): - if settings.is_subscription_tariff_mode(): - texts = get_texts(db_user.language) - await callback.answer( - texts.t("TARIFF_COUNTRY_MANAGEMENT_DISABLED", "ℹ️ Смена серверов недоступна при использовании тарифов"), - show_alert=True, - ) - return - if not await _should_show_countries_management(db_user): texts = get_texts(db_user.language) await callback.answer( @@ -2018,17 +1725,6 @@ async def confirm_change_devices( db_user: User, db: AsyncSession ): - if settings.is_subscription_tariff_mode(): - texts = get_texts(db_user.language) - await callback.answer( - texts.t( - "TARIFF_DEVICE_MANAGEMENT_DISABLED", - "ℹ️ Изменение количества устройств недоступно при использовании тарифов", - ), - show_alert=True, - ) - return - new_devices_count = int(callback.data.split('_')[2]) texts = get_texts(db_user.language) subscription = db_user.subscription @@ -2172,17 +1868,6 @@ async def execute_change_devices( db_user: User, db: AsyncSession ): - if settings.is_subscription_tariff_mode(): - texts = get_texts(db_user.language) - await callback.answer( - texts.t( - "TARIFF_DEVICE_MANAGEMENT_DISABLED", - "ℹ️ Изменение количества устройств недоступно при использовании тарифов", - ), - show_alert=True, - ) - return - callback_parts = callback.data.split('_') new_devices_count = int(callback_parts[3]) price = int(callback_parts[4]) @@ -2788,14 +2473,6 @@ async def handle_reset_traffic( ): from app.config import settings - if settings.is_subscription_tariff_mode(): - texts = get_texts(db_user.language) - await callback.answer( - texts.t("TARIFF_TRAFFIC_MANAGEMENT_DISABLED", "ℹ️ Управление трафиком недоступно при использовании тарифов"), - show_alert=True, - ) - return - if settings.is_traffic_fixed(): await callback.answer("⚠️ В текущем режиме трафик фиксированный и не может быть сброшен", show_alert=True) return @@ -3253,14 +2930,6 @@ async def confirm_reset_traffic( ): from app.config import settings - if settings.is_subscription_tariff_mode(): - texts = get_texts(db_user.language) - await callback.answer( - texts.t("TARIFF_TRAFFIC_MANAGEMENT_DISABLED", "ℹ️ Управление трафиком недоступно при использовании тарифов"), - show_alert=True, - ) - return - if settings.is_traffic_fixed(): await callback.answer("⚠️ В текущем режиме трафик фиксированный", show_alert=True) return @@ -3465,17 +3134,6 @@ async def get_subscription_info_text(subscription, texts, db_user, db: AsyncSess subscription_url = getattr(subscription, 'subscription_url', None) or "Генерируется..." - tariff_name = None - if getattr(subscription, 'tariff_id', None): - try: - await db.refresh(subscription, attribute_names=["tariff"]) - except Exception: - pass - - tariff = getattr(subscription, "tariff", None) - if tariff: - tariff_name = tariff.name - if subscription.is_trial: status_text = "🎁 Тестовая" type_text = "Триал" @@ -3515,9 +3173,6 @@ async def get_subscription_info_text(subscription, texts, db_user, db: AsyncSess if subscription_cost > 0: info_text += f"\n💰 Стоимость подписки в месяц: {texts.format_price(subscription_cost)}" - if tariff_name: - info_text += f"\n📦 Тариф: {tariff_name}" - if ( subscription_url and subscription_url != "Генерируется..." @@ -3590,14 +3245,6 @@ async def select_country( db_user: User, db: AsyncSession ): - if settings.is_subscription_tariff_mode(): - texts = get_texts(db_user.language) - await callback.answer( - texts.t("TARIFF_COUNTRY_MANAGEMENT_DISABLED", "ℹ️ Смена серверов недоступна при использовании тарифов"), - show_alert=True, - ) - return - country_uuid = callback.data.split('_')[1] data = await state.get_data() @@ -3653,14 +3300,6 @@ async def countries_continue( state: FSMContext, db_user: User ): - if settings.is_subscription_tariff_mode(): - texts = get_texts(db_user.language) - await callback.answer( - texts.t("TARIFF_COUNTRY_MANAGEMENT_DISABLED", "ℹ️ Смена серверов недоступна при использовании тарифов"), - show_alert=True, - ) - return - data = await state.get_data() texts = get_texts(db_user.language) @@ -4085,7 +3724,6 @@ async def confirm_purchase( existing_subscription.traffic_limit_gb = final_traffic_gb existing_subscription.device_limit = data['devices'] existing_subscription.connected_squads = data['countries'] - existing_subscription.tariff_id = data.get('tariff_id') existing_subscription.start_date = current_time existing_subscription.end_date = current_time + timedelta(days=data['period_days']) + bonus_period @@ -4105,8 +3743,7 @@ async def confirm_purchase( duration_days=data['period_days'], device_limit=data['devices'], connected_squads=data['countries'], - traffic_gb=final_traffic_gb, - tariff_id=data.get('tariff_id'), + traffic_gb=final_traffic_gb ) from app.utils.user_utils import mark_user_as_had_paid_subscription @@ -4352,14 +3989,6 @@ async def add_traffic( db_user: User, db: AsyncSession ): - if settings.is_subscription_tariff_mode(): - texts = get_texts(db_user.language) - await callback.answer( - texts.t("TARIFF_TRAFFIC_MANAGEMENT_DISABLED", "ℹ️ Управление трафиком недоступно при использовании тарифов"), - show_alert=True, - ) - return - if settings.is_traffic_fixed(): await callback.answer("⚠️ В текущем режиме трафик фиксированный", show_alert=True) return @@ -4493,8 +4122,7 @@ async def create_paid_subscription_with_traffic_mode( duration_days: int, device_limit: int, connected_squads: List[str], - traffic_gb: Optional[int] = None, - tariff_id: Optional[int] = None, + traffic_gb: Optional[int] = None ): from app.config import settings @@ -4512,8 +4140,7 @@ async def create_paid_subscription_with_traffic_mode( duration_days=duration_days, traffic_limit_gb=traffic_limit_gb, device_limit=device_limit, - connected_squads=connected_squads, - tariff_id=tariff_id, + connected_squads=connected_squads ) logger.info(f"📋 Создана подписка с трафиком: {traffic_limit_gb} ГБ (режим: {settings.TRAFFIC_SELECTION_MODE})") @@ -4551,39 +4178,11 @@ async def handle_subscription_settings( devices_used = await get_current_devices_count(db_user) - tariff_line = "" - if settings.is_subscription_tariff_mode(): - tariff_name = None - try: - await db.refresh(subscription, attribute_names=["tariff"]) - except Exception: - pass - - tariff = getattr(subscription, "tariff", None) - if tariff: - tariff_name = tariff.name - elif getattr(subscription, "tariff_id", None): - try: - from app.database.crud.tariff import get_tariff_by_id - - tariff_model = await get_tariff_by_id(db, subscription.tariff_id, include_inactive=True) - if tariff_model: - tariff_name = tariff_model.name - except Exception: - tariff_name = None - - if tariff_name: - tariff_line = texts.t( - "SUBSCRIPTION_SETTINGS_TARIFF_LINE", - "📦 Тариф: {tariff}\n", - ).format(tariff=tariff_name) - settings_text = texts.t( "SUBSCRIPTION_SETTINGS_OVERVIEW", ( "⚙️ Настройки подписки\n\n" "📊 Текущие параметры:\n" - "{tariff_line}" "🌐 Стран: {countries_count}\n" "📈 Трафик: {traffic_used} / {traffic_limit}\n" "📱 Устройства: {devices_used} / {devices_limit}\n\n" @@ -4595,7 +4194,6 @@ async def handle_subscription_settings( traffic_limit=texts.format_traffic(subscription.traffic_limit_gb), devices_used=devices_used, devices_limit=subscription.device_limit, - tariff_line=tariff_line, ) show_countries = await _should_show_countries_management(db_user) @@ -4958,9 +4556,6 @@ async def handle_add_country_to_subscription( async def _should_show_countries_management(user: Optional[User] = None) -> bool: - if settings.is_subscription_tariff_mode(): - return False - try: promo_group_id = user.promo_group_id if user else None @@ -5002,14 +4597,6 @@ async def confirm_add_countries_to_subscription( db: AsyncSession, state: FSMContext ): - if settings.is_subscription_tariff_mode(): - texts = get_texts(db_user.language) - await callback.answer( - texts.t("TARIFF_COUNTRY_MANAGEMENT_DISABLED", "ℹ️ Смена серверов недоступна при использовании тарифов"), - show_alert=True, - ) - return - data = await state.get_data() texts = get_texts(db_user.language) subscription = db_user.subscription @@ -6005,14 +5592,6 @@ async def handle_switch_traffic( ): from app.config import settings - if settings.is_subscription_tariff_mode(): - texts = get_texts(db_user.language) - await callback.answer( - texts.t("TARIFF_TRAFFIC_MANAGEMENT_DISABLED", "ℹ️ Управление трафиком недоступно при использовании тарифов"), - show_alert=True, - ) - return - if settings.is_traffic_fixed(): await callback.answer("⚠️ В текущем режиме трафик фиксированный", show_alert=True) return @@ -6056,14 +5635,6 @@ async def confirm_switch_traffic( db_user: User, db: AsyncSession ): - if settings.is_subscription_tariff_mode(): - texts = get_texts(db_user.language) - await callback.answer( - texts.t("TARIFF_TRAFFIC_MANAGEMENT_DISABLED", "ℹ️ Управление трафиком недоступно при использовании тарифов"), - show_alert=True, - ) - return - new_traffic_gb = int(callback.data.split('_')[2]) texts = get_texts(db_user.language) subscription = db_user.subscription @@ -6177,14 +5748,6 @@ async def execute_switch_traffic( db_user: User, db: AsyncSession ): - if settings.is_subscription_tariff_mode(): - texts = get_texts(db_user.language) - await callback.answer( - texts.t("TARIFF_TRAFFIC_MANAGEMENT_DISABLED", "ℹ️ Управление трафиком недоступно при использовании тарифов"), - show_alert=True, - ) - return - callback_parts = callback.data.split('_') new_traffic_gb = int(callback_parts[3]) price_difference = int(callback_parts[4]) @@ -6389,26 +5952,6 @@ def register_handlers(dp: Dispatcher): F.data.in_(["menu_buy", "subscription_upgrade"]) ) - dp.callback_query.register( - handle_tariff_selection, - F.data.startswith("tariff_select_") - ) - - dp.callback_query.register( - handle_tariff_period_selection, - F.data.startswith("tariff_period_") - ) - - dp.callback_query.register( - handle_tariff_back, - F.data == "tariff_back" - ) - - dp.callback_query.register( - handle_change_tariff, - F.data == "subscription_change_tariff" - ) - dp.callback_query.register( handle_add_countries, F.data == "subscription_add_countries" diff --git a/app/keyboards/inline.py b/app/keyboards/inline.py index e1de21c2..f2f89624 100644 --- a/app/keyboards/inline.py +++ b/app/keyboards/inline.py @@ -1,4 +1,4 @@ -from typing import Iterable, List, Optional, Sequence +from typing import List, Optional from aiogram import types from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton from datetime import datetime @@ -635,7 +635,7 @@ def get_trial_keyboard(language: str = "ru") -> InlineKeyboardMarkup: def get_subscription_period_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup: texts = get_texts(language) keyboard = [] - + available_periods = settings.get_available_subscription_periods() period_texts = { @@ -659,91 +659,14 @@ def get_subscription_period_keyboard(language: str = DEFAULT_LANGUAGE) -> Inline keyboard.append([ InlineKeyboardButton(text=texts.BACK, callback_data="back_to_menu") ]) - - return InlineKeyboardMarkup(inline_keyboard=keyboard) - - -def get_tariff_selection_keyboard( - tariffs: Sequence["SubscriptionTariff"], - language: str = DEFAULT_LANGUAGE, -) -> InlineKeyboardMarkup: - texts = get_texts(language) - keyboard: List[List[InlineKeyboardButton]] = [] - - for tariff in tariffs: - if not getattr(tariff, "prices", None): - continue - - price_values = [price.price_kopeks for price in tariff.prices if price.price_kopeks is not None] - if price_values: - min_price = min(price_values) - button_text = texts.t( - "TARIFF_SELECT_BUTTON", - "{name} • от {price}", - ).format(name=tariff.name, price=texts.format_price(min_price)) - else: - button_text = tariff.name - - keyboard.append([ - InlineKeyboardButton( - text=button_text, - callback_data=f"tariff_select_{tariff.id}", - ) - ]) - - if not keyboard: - keyboard.append([ - InlineKeyboardButton( - text=texts.t("NO_TARIFFS_AVAILABLE", "❌ Тарифы недоступны"), - callback_data="no_tariffs", - ) - ]) - - keyboard.append([ - InlineKeyboardButton(text=texts.BACK, callback_data="menu_subscription") - ]) - - return InlineKeyboardMarkup(inline_keyboard=keyboard) - - -def get_tariff_period_keyboard( - tariff: "SubscriptionTariff", - language: str = DEFAULT_LANGUAGE, -) -> InlineKeyboardMarkup: - texts = get_texts(language) - keyboard: List[List[InlineKeyboardButton]] = [] - - sorted_prices = sorted( - getattr(tariff, "prices", []), - key=lambda price: price.period_days, - ) - - for price in sorted_prices: - period_text = format_period_description(price.period_days, language) - button_text = texts.t( - "TARIFF_PERIOD_BUTTON", - "{period} • {price}", - ).format(period=period_text, price=texts.format_price(price.price_kopeks)) - - keyboard.append([ - InlineKeyboardButton( - text=button_text, - callback_data=f"tariff_period_{tariff.id}_{price.period_days}", - ) - ]) - - keyboard.append([ - InlineKeyboardButton(text=texts.BACK, callback_data="tariff_back"), - InlineKeyboardButton(text=texts.CANCEL, callback_data="subscription_cancel"), - ]) - + return InlineKeyboardMarkup(inline_keyboard=keyboard) def get_traffic_packages_keyboard(language: str = DEFAULT_LANGUAGE) -> InlineKeyboardMarkup: import logging logger = logging.getLogger(__name__) - + from app.config import settings if settings.is_traffic_fixed(): @@ -1839,34 +1762,10 @@ def get_devices_management_keyboard( def get_updated_subscription_settings_keyboard(language: str = DEFAULT_LANGUAGE, show_countries_management: bool = True) -> InlineKeyboardMarkup: from app.config import settings - + texts = get_texts(language) - if settings.is_subscription_tariff_mode(): - keyboard = [ - [ - InlineKeyboardButton( - text=texts.t("CHANGE_TARIFF_BUTTON", "🔁 Сменить тариф"), - callback_data="subscription_change_tariff", - ) - ], - [ - InlineKeyboardButton( - text=texts.t("MANAGE_DEVICES_BUTTON", "🔧 Управление устройствами"), - callback_data="subscription_manage_devices", - ) - ], - [ - InlineKeyboardButton( - text=texts.t("RESET_ALL_DEVICES_BUTTON", "🔄 Сбросить все устройства"), - callback_data="reset_all_devices", - ) - ], - [InlineKeyboardButton(text=texts.BACK, callback_data="menu_subscription")], - ] - return InlineKeyboardMarkup(inline_keyboard=keyboard) - keyboard = [] - + if show_countries_management: keyboard.append([ InlineKeyboardButton(text=texts.t("ADD_COUNTRIES_BUTTON", "🌐 Добавить страны"), callback_data="subscription_add_countries") @@ -1874,7 +1773,7 @@ def get_updated_subscription_settings_keyboard(language: str = DEFAULT_LANGUAGE, keyboard.extend([ [ - InlineKeyboardButton(text=texts.t("CHANGE_DEVICES_BUTTON", "📱 Изменить устройства"), callback_data="subscription_change_devices") + InlineKeyboardButton(text=texts.t("CHANGE_DEVICES_BUTTON", "📱 Изменить устройства"), callback_data="subscription_change_devices") ], [ InlineKeyboardButton(text=texts.t("MANAGE_DEVICES_BUTTON", "🔧 Управление устройствами"), callback_data="subscription_manage_devices") @@ -1888,11 +1787,11 @@ def get_updated_subscription_settings_keyboard(language: str = DEFAULT_LANGUAGE, keyboard.insert(-2, [ InlineKeyboardButton(text=texts.t("RESET_TRAFFIC_BUTTON", "🔄 Сбросить трафик"), callback_data="subscription_reset_traffic") ]) - + keyboard.append([ InlineKeyboardButton(text=texts.BACK, callback_data="menu_subscription") ]) - + return InlineKeyboardMarkup(inline_keyboard=keyboard) diff --git a/app/services/tariff_service.py b/app/services/tariff_service.py deleted file mode 100644 index b78484c3..00000000 --- a/app/services/tariff_service.py +++ /dev/null @@ -1,45 +0,0 @@ -from typing import List, Optional - -from sqlalchemy.ext.asyncio import AsyncSession - -from app.database.crud.tariff import ( - get_active_tariffs_for_promo_group, - get_tariff_by_id, -) -from app.database.models import SubscriptionTariff, User - - -class TariffService: - @staticmethod - async def get_available_tariffs( - db: AsyncSession, - user: Optional[User], - ) -> List[SubscriptionTariff]: - promo_group_id = getattr(user, "promo_group_id", None) if user else None - tariffs = await get_active_tariffs_for_promo_group(db, promo_group_id) - return sorted(tariffs, key=lambda tariff: (tariff.sort_order, tariff.id)) - - @staticmethod - async def get_tariff_for_user( - db: AsyncSession, - tariff_id: int, - user: Optional[User], - ) -> Optional[SubscriptionTariff]: - promo_group_id = getattr(user, "promo_group_id", None) if user else None - tariff = await get_tariff_by_id(db, tariff_id, include_inactive=False) - if not tariff: - return None - - available_servers = [ - server - for server in tariff.server_squads - if server.is_available and not server.is_full - ] - if not available_servers: - return None - - if not tariff.is_available_for_promo_group(promo_group_id): - return None - - tariff.server_squads = available_servers - return tariff diff --git a/app/states.py b/app/states.py index dac39fea..43655b35 100644 --- a/app/states.py +++ b/app/states.py @@ -6,8 +6,6 @@ class RegistrationStates(StatesGroup): waiting_for_referral_code = State() class SubscriptionStates(StatesGroup): - selecting_tariff = State() - selecting_tariff_period = State() selecting_period = State() selecting_traffic = State() selecting_countries = State() diff --git a/app/webapi/app.py b/app/webapi/app.py index db2d54fd..0761ecad 100644 --- a/app/webapi/app.py +++ b/app/webapi/app.py @@ -17,7 +17,6 @@ from .routes import ( remnawave, stats, subscriptions, - tariffs, tickets, tokens, transactions, @@ -46,10 +45,6 @@ OPENAPI_TAGS = [ "name": "subscriptions", "description": "Создание, продление и настройка подписок бота.", }, - { - "name": "tariffs", - "description": "Управление преднастроенными тарифами подписок.", - }, { "name": "support", "description": "Работа с тикетами поддержки, приоритетами и ограничениями на ответы.", @@ -106,7 +101,6 @@ def create_web_api_app() -> FastAPI: app.include_router(config.router, prefix="/settings", tags=["settings"]) app.include_router(users.router, prefix="/users", tags=["users"]) app.include_router(subscriptions.router, prefix="/subscriptions", tags=["subscriptions"]) - app.include_router(tariffs.router, prefix="/tariffs", tags=["tariffs"]) app.include_router(tickets.router, prefix="/tickets", tags=["support"]) app.include_router(transactions.router, prefix="/transactions", tags=["transactions"]) app.include_router(promo_groups.router, prefix="/promo-groups", tags=["promo-groups"]) diff --git a/app/webapi/routes/__init__.py b/app/webapi/routes/__init__.py index ed548ae9..31aef685 100644 --- a/app/webapi/routes/__init__.py +++ b/app/webapi/routes/__init__.py @@ -5,7 +5,6 @@ from . import ( remnawave, stats, subscriptions, - tariffs, tickets, tokens, transactions, @@ -19,7 +18,6 @@ __all__ = [ "remnawave", "stats", "subscriptions", - "tariffs", "tickets", "tokens", "transactions", diff --git a/app/webapi/routes/subscriptions.py b/app/webapi/routes/subscriptions.py index 01ab9a88..2ec77fe2 100644 --- a/app/webapi/routes/subscriptions.py +++ b/app/webapi/routes/subscriptions.py @@ -50,7 +50,6 @@ def _serialize_subscription(subscription: Subscription) -> SubscriptionResponse: subscription_url=subscription.subscription_url, subscription_crypto_link=subscription.subscription_crypto_link, connected_squads=list(subscription.connected_squads or []), - tariff_id=subscription.tariff_id, created_at=subscription.created_at, updated_at=subscription.updated_at, ) @@ -59,10 +58,7 @@ def _serialize_subscription(subscription: Subscription) -> SubscriptionResponse: async def _get_subscription(db: AsyncSession, subscription_id: int) -> Subscription: result = await db.execute( select(Subscription) - .options( - selectinload(Subscription.user), - selectinload(Subscription.tariff), - ) + .options(selectinload(Subscription.user)) .where(Subscription.id == subscription_id) ) subscription = result.scalar_one_or_none() @@ -81,10 +77,7 @@ async def list_subscriptions( user_id: Optional[int] = Query(default=None), is_trial: Optional[bool] = Query(default=None), ) -> list[SubscriptionResponse]: - query = select(Subscription).options( - selectinload(Subscription.user), - selectinload(Subscription.tariff), - ) + query = select(Subscription).options(selectinload(Subscription.user)) if status_filter: query = query.where(Subscription.status == status_filter.value) @@ -138,7 +131,6 @@ async def create_subscription( traffic_limit_gb=payload.traffic_limit_gb or settings.DEFAULT_TRAFFIC_LIMIT_GB, device_limit=payload.device_limit or settings.DEFAULT_DEVICE_LIMIT, connected_squads=payload.connected_squads or [], - tariff_id=payload.tariff_id, ) subscription = await _get_subscription(db, subscription.id) diff --git a/app/webapi/routes/tariffs.py b/app/webapi/routes/tariffs.py deleted file mode 100644 index 36aa20f6..00000000 --- a/app/webapi/routes/tariffs.py +++ /dev/null @@ -1,122 +0,0 @@ -from fastapi import APIRouter, Depends, HTTPException, Security, status -from sqlalchemy.ext.asyncio import AsyncSession - -from app.database.crud.tariff import ( - create_tariff, - delete_tariff, - get_tariff_by_id, - list_tariffs, - update_tariff, -) -from app.database.models import SubscriptionTariff - -from ..dependencies import get_db_session, require_api_token -from ..schemas.tariffs import ( - TariffCreateRequest, - TariffResponse, - TariffUpdateRequest, - TariffPricePayload, -) - -router = APIRouter() - - -def _serialize_tariff(tariff: SubscriptionTariff) -> TariffResponse: - return TariffResponse( - id=tariff.id, - name=tariff.name, - description=tariff.description, - traffic_limit_gb=tariff.traffic_limit_gb, - device_limit=tariff.device_limit, - is_active=tariff.is_active, - sort_order=tariff.sort_order, - server_squads=[server.squad_uuid for server in tariff.server_squads if getattr(server, "squad_uuid", None)], - promo_group_ids=[group.id for group in tariff.promo_groups if group is not None], - prices=[ - TariffPricePayload(period_days=price.period_days, price_kopeks=price.price_kopeks) - for price in sorted(tariff.prices, key=lambda item: item.period_days) - ], - created_at=tariff.created_at, - updated_at=tariff.updated_at, - ) - - -@router.get("", response_model=list[TariffResponse], tags=["tariffs"]) -async def list_subscription_tariffs( - include_inactive: bool = False, - _: str = Security(require_api_token), - db: AsyncSession = Depends(get_db_session), -) -> list[TariffResponse]: - tariffs = await list_tariffs(db, include_inactive=include_inactive) - return [_serialize_tariff(tariff) for tariff in tariffs] - - -@router.get("/{tariff_id}", response_model=TariffResponse, tags=["tariffs"]) -async def get_subscription_tariff( - tariff_id: int, - _: str = Security(require_api_token), - db: AsyncSession = Depends(get_db_session), -) -> TariffResponse: - tariff = await get_tariff_by_id(db, tariff_id, include_inactive=True) - if not tariff: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tariff not found") - return _serialize_tariff(tariff) - - -@router.post("", response_model=TariffResponse, status_code=status.HTTP_201_CREATED, tags=["tariffs"]) -async def create_subscription_tariff( - payload: TariffCreateRequest, - _: str = Security(require_api_token), - db: AsyncSession = Depends(get_db_session), -) -> TariffResponse: - tariff = await create_tariff( - db, - name=payload.name, - description=payload.description, - traffic_limit_gb=payload.traffic_limit_gb, - device_limit=payload.device_limit, - server_uuids=payload.server_squads, - promo_group_ids=payload.promo_group_ids, - prices=[price.model_dump() for price in payload.prices], - is_active=payload.is_active, - sort_order=payload.sort_order, - ) - return _serialize_tariff(tariff) - - -@router.put("/{tariff_id}", response_model=TariffResponse, tags=["tariffs"]) -async def update_subscription_tariff( - tariff_id: int, - payload: TariffUpdateRequest, - _: str = Security(require_api_token), - db: AsyncSession = Depends(get_db_session), -) -> TariffResponse: - tariff = await update_tariff( - db, - tariff_id, - name=payload.name, - description=payload.description, - traffic_limit_gb=payload.traffic_limit_gb, - device_limit=payload.device_limit, - server_uuids=payload.server_squads, - promo_group_ids=payload.promo_group_ids, - prices=[price.model_dump() for price in payload.prices] if payload.prices is not None else None, - is_active=payload.is_active, - sort_order=payload.sort_order, - ) - if not tariff: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tariff not found") - return _serialize_tariff(tariff) - - -@router.delete("/{tariff_id}", status_code=status.HTTP_204_NO_CONTENT, tags=["tariffs"]) -async def delete_subscription_tariff( - tariff_id: int, - _: str = Security(require_api_token), - db: AsyncSession = Depends(get_db_session), -) -> None: - success = await delete_tariff(db, tariff_id) - if not success: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Unable to delete tariff") - - return None diff --git a/app/webapi/schemas/subscriptions.py b/app/webapi/schemas/subscriptions.py index 08691023..f09b5405 100644 --- a/app/webapi/schemas/subscriptions.py +++ b/app/webapi/schemas/subscriptions.py @@ -22,7 +22,6 @@ class SubscriptionResponse(BaseModel): subscription_url: Optional[str] = None subscription_crypto_link: Optional[str] = None connected_squads: List[str] = Field(default_factory=list) - tariff_id: Optional[int] = None created_at: datetime updated_at: datetime @@ -35,7 +34,6 @@ class SubscriptionCreateRequest(BaseModel): device_limit: Optional[int] = None squad_uuid: Optional[str] = None connected_squads: Optional[List[str]] = None - tariff_id: Optional[int] = None class SubscriptionExtendRequest(BaseModel): diff --git a/app/webapi/schemas/tariffs.py b/app/webapi/schemas/tariffs.py deleted file mode 100644 index 1885ff9d..00000000 --- a/app/webapi/schemas/tariffs.py +++ /dev/null @@ -1,48 +0,0 @@ -from datetime import datetime -from typing import List, Optional - -from pydantic import BaseModel, Field - - -class TariffPricePayload(BaseModel): - period_days: int = Field(..., gt=0) - price_kopeks: int = Field(..., ge=0) - - -class TariffResponse(BaseModel): - id: int - name: str - description: Optional[str] = None - traffic_limit_gb: int - device_limit: int - is_active: bool - sort_order: int - server_squads: List[str] = Field(default_factory=list) - promo_group_ids: List[int] = Field(default_factory=list) - prices: List[TariffPricePayload] = Field(default_factory=list) - created_at: datetime - updated_at: datetime - - -class TariffCreateRequest(BaseModel): - name: str - description: Optional[str] = None - traffic_limit_gb: int = Field(..., ge=0) - device_limit: int = Field(..., ge=1) - is_active: bool = True - sort_order: int = 0 - server_squads: List[str] = Field(default_factory=list) - promo_group_ids: Optional[List[int]] = None - prices: List[TariffPricePayload] = Field(default_factory=list) - - -class TariffUpdateRequest(BaseModel): - name: Optional[str] = None - description: Optional[str] = None - traffic_limit_gb: Optional[int] = Field(default=None, ge=0) - device_limit: Optional[int] = Field(default=None, ge=1) - is_active: Optional[bool] = None - sort_order: Optional[int] = None - server_squads: Optional[List[str]] = None - promo_group_ids: Optional[List[int]] = None - prices: Optional[List[TariffPricePayload]] = None