feat: premium squads inside one tariff

This commit is contained in:
3252a8
2026-05-10 12:51:53 +03:00
parent 6f9a87cf98
commit 990eba1e1e
34 changed files with 1898 additions and 135 deletions
+275 -8
View File
@@ -8,7 +8,7 @@ from bot.middlewares.i18n import JsonI18n
from db.dal import user_dal, subscription_dal, promo_code_dal, payment_dal, user_billing_dal, tariff_dal
from config.tariffs_config import Tariff
from bot.utils.date_utils import add_months
from bot.utils.date_utils import add_months, month_start
from bot.utils.config_link import prepare_config_links
from db.models import User, Subscription
@@ -31,6 +31,7 @@ class SubscriptionService:
self.panel_service = panel_service
self.bot = bot
self.i18n = i18n
self._premium_access_cache: Dict[Tuple[str, ...], Dict[str, Any]] = {}
@staticmethod
def gb_to_bytes(gb: float) -> int:
@@ -71,9 +72,17 @@ class SubscriptionService:
raise ValueError(f"Tariff {tariff.key} is {tariff.billing_model}, expected {billing_model}")
return tariff
def _panel_squads_for_tariff(self, tariff: Optional[Tariff]) -> Optional[List[str]]:
def _panel_squads_for_tariff(
self,
tariff: Optional[Tariff],
*,
include_premium: bool = True,
) -> Optional[List[str]]:
if tariff:
return tariff.squad_uuids
squads = list(tariff.squad_uuids or [])
if include_premium:
squads.extend(tariff.premium_squad_uuids or [])
return list(dict.fromkeys(squads))
return self.settings.parsed_user_squad_uuids
def _traffic_limit_for_period_tariff(self, tariff: Optional[Tariff], topup_balance_bytes: int = 0) -> int:
@@ -81,6 +90,85 @@ class SubscriptionService:
return int(tariff.monthly_bytes + max(0, topup_balance_bytes))
return self.settings.user_traffic_limit_bytes
def _premium_limit_for_tariff(self, tariff: Optional[Tariff], topup_balance_bytes: int = 0) -> int:
if not tariff:
return 0
return int(tariff.premium_monthly_bytes + max(0, topup_balance_bytes))
@staticmethod
def _premium_effective_limit_bytes(
premium_baseline_bytes: int,
premium_topup_balance_bytes: int = 0,
premium_topup_used_bytes: int = 0,
) -> int:
return int(premium_baseline_bytes or 0) + max(0, int(premium_topup_balance_bytes or 0)) + max(
0, int(premium_topup_used_bytes or 0)
)
async def premium_access_for_tariff(self, tariff: Optional[Tariff]) -> Dict[str, Any]:
if not tariff or not tariff.premium_squad_uuids:
return {"squad_uuids": [], "squad_labels": [], "node_labels": []}
cache_key = tuple(sorted(str(uuid) for uuid in tariff.premium_squad_uuids))
now_ts = datetime.now(timezone.utc).timestamp()
cached = self._premium_access_cache.get(cache_key)
if cached and now_ts - float(cached.get("ts", 0)) < 600:
return {
"squad_uuids": list(cached.get("squad_uuids") or []),
"squad_labels": list(cached.get("squad_labels") or []),
"node_labels": list(cached.get("node_labels") or []),
}
squad_name_map: Dict[str, str] = {}
try:
squads = await self.panel_service.get_internal_squads() or []
for squad in squads:
if not isinstance(squad, dict):
continue
squad_uuid = str(squad.get("uuid") or squad.get("id") or "")
if not squad_uuid:
continue
squad_name_map[squad_uuid] = str(squad.get("name") or squad.get("title") or squad_uuid)
except Exception:
logging.debug("Failed to load internal squad names for premium display", exc_info=True)
node_labels: List[str] = []
for squad_uuid in tariff.premium_squad_uuids:
try:
nodes = await self.panel_service.get_internal_squad_accessible_nodes(squad_uuid) or []
except Exception:
logging.debug("Failed to load accessible nodes for premium squad %s", squad_uuid, exc_info=True)
nodes = []
for node in nodes:
if not isinstance(node, dict):
continue
node_uuid = str(node.get("uuid") or node.get("nodeUuid") or node.get("node_uuid") or "")
node_name = str(node.get("name") or node.get("address") or node.get("host") or "").strip()
if node_name:
label = node_name
elif node_uuid:
label = f"{node_uuid[:8]}..."
else:
continue
node_labels.append(label)
squad_labels = [
squad_name_map.get(str(uuid), f"{str(uuid)[:8]}...")
for uuid in tariff.premium_squad_uuids
]
payload = {
"ts": now_ts,
"squad_uuids": list(tariff.premium_squad_uuids),
"squad_labels": list(dict.fromkeys(squad_labels)),
"node_labels": list(dict.fromkeys(node_labels)),
}
self._premium_access_cache[cache_key] = payload
return {
"squad_uuids": list(payload["squad_uuids"]),
"squad_labels": list(payload["squad_labels"]),
"node_labels": list(payload["node_labels"]),
}
def _base_hwid_limit_for_tariff(self, tariff: Optional[Tariff]) -> Optional[int]:
if tariff and tariff.hwid_device_limit is not None:
return int(tariff.hwid_device_limit)
@@ -649,6 +737,12 @@ class SubscriptionService:
"tariff_key": tariff.key if tariff else None,
"tier_baseline_bytes": 0,
"topup_balance_bytes": new_balance,
"premium_baseline_bytes": self._premium_limit_for_tariff(tariff, 0),
"premium_topup_balance_bytes": 0,
"premium_topup_used_bytes": 0,
"premium_used_bytes": 0,
"premium_is_limited": False,
"premium_period_start_at": None,
"period_start_at": None,
"is_throttled": False,
"effective_monthly_price_rub": None,
@@ -671,7 +765,7 @@ class SubscriptionService:
hwid_device_limit=effective_hwid_limit,
)
if tariff:
panel_update_payload["activeInternalSquads"] = tariff.squad_uuids
panel_update_payload["activeInternalSquads"] = self._panel_squads_for_tariff(tariff)
panel_update_payload.update(self._panel_identity_payload_for_user(db_user))
@@ -785,7 +879,10 @@ class SubscriptionService:
traffic_limit_bytes=new_limit,
hwid_device_limit=effective_hwid_limit,
)
panel_payload["activeInternalSquads"] = tariff.squad_uuids
panel_payload["activeInternalSquads"] = self._panel_squads_for_tariff(
tariff,
include_premium=not bool(getattr(updated_sub, "premium_is_limited", False)),
)
panel_payload.update(self._panel_identity_payload_for_user(db_user))
await self.panel_service.update_user_details_on_panel(db_user.panel_user_uuid, panel_payload)
await tariff_dal.create_traffic_topup(
@@ -802,6 +899,93 @@ class SubscriptionService:
"tariff_key": tariff.key,
}
async def activate_premium_topup(
self,
session: AsyncSession,
user_id: int,
tariff_key: str,
traffic_gb: float,
payment_amount: float,
payment_db_id: int,
provider: str = "yookassa",
) -> Optional[Dict[str, Any]]:
tariff = self._resolve_tariff(tariff_key)
if not tariff or not tariff.premium_squad_uuids:
logging.error("Premium top-up requires a tariff with premium squads for user %s", user_id)
return None
await self._record_payment_context(
session,
payment_db_id,
sale_mode="premium_topup",
tariff_key=tariff.key,
purchased_gb=float(traffic_gb),
)
db_user = await user_dal.get_user_by_id(session, user_id)
if not db_user or not db_user.panel_user_uuid:
return None
sub = await subscription_dal.get_active_subscription_by_user_id(session, user_id, db_user.panel_user_uuid)
if not sub:
return None
purchase_bytes = self.gb_to_bytes(traffic_gb)
now = datetime.now(timezone.utc)
premium_period_start = month_start(now)
current_period_start = getattr(sub, "premium_period_start_at", None)
same_period = bool(current_period_start and current_period_start == premium_period_start)
previous_topup_used = int(sub.premium_topup_used_bytes or 0) if same_period else 0
premium_used = int(sub.premium_used_bytes or 0) if same_period else 0
premium_baseline = int(tariff.premium_monthly_bytes or sub.premium_baseline_bytes or 0)
premium_topup_balance = int(sub.premium_topup_balance_bytes or 0) + purchase_bytes
overflow_to_cover = max(0, premium_used - premium_baseline - previous_topup_used)
consume_now = min(premium_topup_balance, overflow_to_cover)
premium_topup_balance -= consume_now
premium_topup_used = previous_topup_used + consume_now
premium_limit = self._premium_effective_limit_bytes(
premium_baseline,
premium_topup_balance,
premium_topup_used,
)
premium_is_limited = premium_limit > 0 and premium_used >= premium_limit
updated_sub = await subscription_dal.update_subscription(
session,
sub.subscription_id,
{
"premium_baseline_bytes": premium_baseline,
"premium_topup_balance_bytes": premium_topup_balance,
"premium_topup_used_bytes": premium_topup_used,
"premium_used_bytes": premium_used,
"premium_is_limited": premium_is_limited,
"premium_period_start_at": premium_period_start,
"tariff_key": tariff.key,
},
)
panel_payload = {
"uuid": db_user.panel_user_uuid,
"activeInternalSquads": self._panel_squads_for_tariff(
tariff,
include_premium=not premium_is_limited,
),
}
await self.panel_service.update_user_details_on_panel(db_user.panel_user_uuid, panel_payload)
await tariff_dal.create_traffic_topup(
session,
subscription_id=sub.subscription_id,
payment_id=payment_db_id,
purchased_bytes=purchase_bytes,
kind="premium_topup",
)
return {
"subscription_id": sub.subscription_id,
"premium_limit_bytes": premium_limit,
"premium_topup_balance_bytes": premium_topup_balance,
"premium_topup_used_bytes": premium_topup_used,
"premium_is_limited": premium_is_limited,
"tariff_key": tariff.key,
}
async def activate_hwid_device_topup(
self,
session: AsyncSession,
@@ -966,7 +1150,23 @@ class SubscriptionService:
before_tariff_key = sub.tariff_key
options = self.calculate_tariff_switch_options(sub, target)
now = datetime.now(timezone.utc)
update_data: Dict[str, Any] = {"tariff_key": target.key, "is_throttled": False}
premium_topup_balance = int(sub.premium_topup_balance_bytes or 0)
premium_topup_used = int(getattr(sub, "premium_topup_used_bytes", 0) or 0)
premium_baseline = target.premium_monthly_bytes
premium_limit = self._premium_effective_limit_bytes(
premium_baseline,
premium_topup_balance,
premium_topup_used,
)
premium_used = int(sub.premium_used_bytes or 0)
update_data: Dict[str, Any] = {
"tariff_key": target.key,
"is_throttled": False,
"premium_baseline_bytes": premium_baseline,
"premium_topup_balance_bytes": premium_topup_balance,
"premium_topup_used_bytes": premium_topup_used,
"premium_is_limited": bool(premium_limit > 0 and premium_used >= premium_limit),
}
converted_bytes = None
base_hwid_limit = self._base_hwid_limit_for_tariff(target)
extra_hwid_devices = int(sub.extra_hwid_devices or 0)
@@ -1011,7 +1211,10 @@ class SubscriptionService:
traffic_limit_strategy="NO_RESET" if target.billing_model == "traffic" else "MONTH",
hwid_device_limit=self._effective_hwid_limit(base_hwid_limit, extra_hwid_devices),
)
panel_payload["activeInternalSquads"] = target.squad_uuids
panel_payload["activeInternalSquads"] = self._panel_squads_for_tariff(
target,
include_premium=not bool(updated.premium_is_limited),
)
panel_payload.update(self._panel_identity_payload_for_user(db_user))
await self.panel_service.update_user_details_on_panel(db_user.panel_user_uuid, panel_payload)
if converted_bytes:
@@ -1092,6 +1295,29 @@ class SubscriptionService:
payment_db_id=payment_db_id,
provider=provider,
)
if sale_mode_base == "premium_topup":
if not tariff_key:
active_user = await user_dal.get_user_by_id(session, user_id)
active_sub = (
await subscription_dal.get_active_subscription_by_user_id(
session, user_id, active_user.panel_user_uuid
)
if active_user and active_user.panel_user_uuid
else None
)
tariff_key = active_sub.tariff_key if active_sub else None
if not tariff_key:
logging.error("Premium top-up activation requires tariff_key for user %s", user_id)
return None
return await self.activate_premium_topup(
session=session,
user_id=user_id,
tariff_key=tariff_key,
traffic_gb=traffic_gb if traffic_gb is not None else float(months),
payment_amount=payment_amount,
payment_db_id=payment_db_id,
provider=provider,
)
if sale_mode_base in {"hwid_device", "hwid_devices"}:
target_devices = int(traffic_gb if traffic_gb is not None else months)
return await self.activate_hwid_device_topup(
@@ -1239,11 +1465,22 @@ class SubscriptionService:
topup_balance_bytes = int(getattr(current_active_sub, "topup_balance_bytes", 0) or 0)
extra_hwid_devices = int(getattr(current_active_sub, "extra_hwid_devices", 0) or 0)
premium_topup_balance_bytes = int(getattr(current_active_sub, "premium_topup_balance_bytes", 0) or 0)
premium_topup_used_bytes = int(getattr(current_active_sub, "premium_topup_used_bytes", 0) or 0)
premium_used_bytes = int(getattr(current_active_sub, "premium_used_bytes", 0) or 0)
premium_period_start_at = getattr(current_active_sub, "premium_period_start_at", None)
tier_baseline_bytes = tariff.monthly_bytes if tariff else self.settings.user_traffic_limit_bytes
premium_baseline_bytes = tariff.premium_monthly_bytes if tariff else 0
premium_limit_bytes = self._premium_effective_limit_bytes(
premium_baseline_bytes,
premium_topup_balance_bytes,
premium_topup_used_bytes,
)
effective_monthly_price = float(payment_amount) / max(1, months_int)
traffic_limit_bytes = self._traffic_limit_for_period_tariff(tariff, topup_balance_bytes)
base_hwid_limit = self._base_hwid_limit_for_tariff(tariff)
effective_hwid_limit = self._effective_hwid_limit(base_hwid_limit, extra_hwid_devices)
premium_is_limited = bool(premium_limit_bytes > 0 and premium_used_bytes >= premium_limit_bytes)
sub_payload = {
"user_id": user_id,
"panel_user_uuid": panel_user_uuid,
@@ -1260,6 +1497,12 @@ class SubscriptionService:
"tariff_key": tariff.key if tariff else None,
"tier_baseline_bytes": tier_baseline_bytes,
"topup_balance_bytes": topup_balance_bytes,
"premium_baseline_bytes": premium_baseline_bytes,
"premium_topup_balance_bytes": premium_topup_balance_bytes,
"premium_topup_used_bytes": premium_topup_used_bytes,
"premium_used_bytes": premium_used_bytes,
"premium_is_limited": premium_is_limited,
"premium_period_start_at": premium_period_start_at,
"period_start_at": None,
"is_throttled": False,
"effective_monthly_price_rub": effective_monthly_price,
@@ -1286,7 +1529,10 @@ class SubscriptionService:
hwid_device_limit=effective_hwid_limit,
)
if tariff:
panel_update_payload["activeInternalSquads"] = tariff.squad_uuids
panel_update_payload["activeInternalSquads"] = self._panel_squads_for_tariff(
tariff,
include_premium=not premium_is_limited,
)
panel_update_payload.update(self._panel_identity_payload_for_user(db_user))
@@ -1552,6 +1798,14 @@ class SubscriptionService:
tariff = None
billing_model_display = tariff.billing_model if tariff else ("traffic" if getattr(self.settings, "traffic_sale_mode", False) else "period")
traffic_limit_strategy = panel_traffic_strategy
premium_access = await self.premium_access_for_tariff(tariff) if tariff else {
"squad_uuids": [],
"squad_labels": [],
"node_labels": [],
}
premium_baseline = int(local_active_sub.premium_baseline_bytes or 0) if local_active_sub else 0
premium_topup_balance = int(local_active_sub.premium_topup_balance_bytes or 0) if local_active_sub else 0
premium_topup_used = int(getattr(local_active_sub, "premium_topup_used_bytes", 0) or 0) if local_active_sub else 0
return {
"user_id": panel_user_data.get("uuid"),
@@ -1568,6 +1822,19 @@ class SubscriptionService:
"billing_model": billing_model_display,
"tier_baseline_bytes": local_active_sub.tier_baseline_bytes if local_active_sub else None,
"topup_balance_bytes": local_active_sub.topup_balance_bytes if local_active_sub else 0,
"premium_baseline_bytes": premium_baseline,
"premium_topup_balance_bytes": premium_topup_balance,
"premium_topup_used_bytes": premium_topup_used,
"premium_used_bytes": local_active_sub.premium_used_bytes if local_active_sub else 0,
"premium_limit_bytes": self._premium_effective_limit_bytes(
premium_baseline,
premium_topup_balance,
premium_topup_used,
),
"premium_is_limited": bool(local_active_sub.premium_is_limited) if local_active_sub else False,
"premium_period_start_at": getattr(local_active_sub, "premium_period_start_at", None) if local_active_sub else None,
"premium_squad_labels": premium_access.get("squad_labels") or [],
"premium_node_labels": premium_access.get("node_labels") or [],
"period_start_at": local_active_sub.period_start_at if local_active_sub else None,
"is_throttled": bool(local_active_sub.is_throttled) if local_active_sub else False,
"base_hwid_device_limit": local_active_sub.hwid_device_limit if local_active_sub else None,