mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-20 03:40:26 +00:00
Revert "Add tariff-based subscription mode"
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user