Revert "Add tariff-based subscription mode"

This commit is contained in:
Egor
2025-10-01 02:06:18 +03:00
committed by GitHub
parent d08f74179f
commit 60d23bedde
14 changed files with 34 additions and 1244 deletions

View File

@@ -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

View File

@@ -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)
)

View File

@@ -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

View File

@@ -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)

View File

@@ -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",
(
"<b>Выберите тариф</b>\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",
(
"<b>{name}</b>\n\n"
"{description}\n\n"
"🌐 <b>Серверы:</b>\n{servers}\n\n"
"📊 <b>Трафик:</b> {traffic}\n"
"📱 <b>Устройства:</b> {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",
(
"<b>{name}</b>\n\n"
"📅 <b>Период:</b> {period}\n"
"📊 <b>Трафик:</b> {traffic}\n"
"📱 <b>Устройства:</b> {devices}\n"
"💰 <b>Стоимость:</b> {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💰 <b>Стоимость подписки в месяц:</b> {texts.format_price(subscription_cost)}"
if tariff_name:
info_text += f"\n📦 <b>Тариф:</b> {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",
(
"⚙️ <b>Настройки подписки</b>\n\n"
"📊 <b>Текущие параметры:</b>\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"

View File

@@ -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)

View File

@@ -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

View File

@@ -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()

View File

@@ -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"])

View File

@@ -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",

View File

@@ -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)

View File

@@ -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

View File

@@ -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):

View File

@@ -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