feat: route purchases through tariff catalog
This commit is contained in:
@@ -78,6 +78,7 @@ class CryptoPayService:
|
||||
|
||||
# Create pending payment in DB and commit to persist
|
||||
try:
|
||||
sale_base = sale_mode.split("@", 1)[0].split("|", 1)[0]
|
||||
payment_record = await payment_dal.create_payment_record(
|
||||
session,
|
||||
{
|
||||
@@ -88,6 +89,9 @@ class CryptoPayService:
|
||||
"description": description,
|
||||
"subscription_duration_months": int(months),
|
||||
"provider": "cryptopay",
|
||||
"sale_mode": sale_mode,
|
||||
"tariff_key": sale_mode.split("@", 1)[1] if "@" in sale_mode else None,
|
||||
"purchased_gb": float(months) if sale_base in {"traffic", "traffic_package", "topup"} else None,
|
||||
},
|
||||
)
|
||||
await session.commit()
|
||||
@@ -103,7 +107,7 @@ class CryptoPayService:
|
||||
"subscription_months": str(months),
|
||||
"payment_db_id": str(payment_record.payment_id),
|
||||
"sale_mode": sale_mode,
|
||||
"traffic_gb": str(months) if sale_mode == "traffic" else None,
|
||||
"traffic_gb": str(months) if sale_base in {"traffic", "traffic_package", "topup"} else None,
|
||||
})
|
||||
try:
|
||||
invoice = await self.client.create_invoice(
|
||||
@@ -151,6 +155,7 @@ class CryptoPayService:
|
||||
months = float(meta.get("subscription_months") or 0)
|
||||
payment_db_id = int(meta["payment_db_id"])
|
||||
sale_mode = meta.get("sale_mode") or ("traffic" if self.settings.traffic_sale_mode else "subscription")
|
||||
sale_base = sale_mode.split("@", 1)[0].split("|", 1)[0]
|
||||
traffic_gb = float(meta.get("traffic_gb")) if meta.get("traffic_gb") else months
|
||||
except Exception:
|
||||
logging.exception("Failed to parse CryptoPay payload.")
|
||||
@@ -174,15 +179,15 @@ class CryptoPayService:
|
||||
activation = await subscription_service.activate_subscription(
|
||||
session,
|
||||
user_id,
|
||||
int(months) if sale_mode != "traffic" else 0,
|
||||
int(months) if sale_base == "subscription" else int(float(traffic_gb)),
|
||||
float(invoice.amount),
|
||||
payment_db_id,
|
||||
provider="cryptopay",
|
||||
sale_mode=sale_mode,
|
||||
traffic_gb=traffic_gb if sale_mode == "traffic" else None,
|
||||
traffic_gb=traffic_gb if sale_base in {"traffic", "traffic_package", "topup"} else None,
|
||||
)
|
||||
referral_bonus = None
|
||||
if sale_mode != "traffic":
|
||||
if sale_base == "subscription":
|
||||
referral_bonus = await referral_service.apply_referral_bonuses_for_payment(
|
||||
session,
|
||||
user_id,
|
||||
@@ -210,7 +215,7 @@ class CryptoPayService:
|
||||
final_end = referral_bonus["referee_new_end_date"]
|
||||
applied_days = referral_bonus.get("referee_bonus_applied_days", 0)
|
||||
|
||||
if sale_mode == "traffic":
|
||||
if sale_base in {"traffic", "traffic_package", "topup"}:
|
||||
text = _("payment_successful_traffic_full",
|
||||
traffic_gb=str(int(traffic_gb)) if float(traffic_gb).is_integer() else f"{traffic_gb:g}",
|
||||
end_date=final_end.strftime('%Y-%m-%d') if final_end else "—",
|
||||
@@ -265,8 +270,8 @@ class CryptoPayService:
|
||||
user_id=user_id,
|
||||
amount=float(invoice.amount),
|
||||
currency=invoice.asset or settings.DEFAULT_CURRENCY_SYMBOL,
|
||||
months=int(months) if sale_mode != "traffic" else 0,
|
||||
traffic_gb=traffic_gb if sale_mode == "traffic" else None,
|
||||
months=int(months) if sale_base == "subscription" else 0,
|
||||
traffic_gb=traffic_gb if sale_base in {"traffic", "traffic_package", "topup"} else None,
|
||||
payment_provider="crypto_pay",
|
||||
username=user.username if user else None
|
||||
)
|
||||
|
||||
@@ -274,22 +274,23 @@ class FreeKassaService:
|
||||
new_status="succeeded",
|
||||
)
|
||||
|
||||
months = payment.subscription_duration_months or 1
|
||||
sale_mode = "traffic" if self.settings.traffic_sale_mode else "subscription"
|
||||
months = payment.purchased_gb or payment.subscription_duration_months or 1
|
||||
sale_mode = payment.sale_mode or ("traffic" if self.settings.traffic_sale_mode else "subscription")
|
||||
sale_base = sale_mode.split("@", 1)[0].split("|", 1)[0]
|
||||
|
||||
activation = await self.subscription_service.activate_subscription(
|
||||
session,
|
||||
payment.user_id,
|
||||
int(months) if sale_mode != "traffic" else 0,
|
||||
int(months) if sale_base == "subscription" else int(float(months)),
|
||||
float(payment.amount),
|
||||
payment.payment_id,
|
||||
provider="freekassa",
|
||||
sale_mode=sale_mode,
|
||||
traffic_gb=months if sale_mode == "traffic" else None,
|
||||
traffic_gb=float(months) if sale_base in {"traffic", "traffic_package", "topup"} else None,
|
||||
)
|
||||
|
||||
referral_bonus = None
|
||||
if sale_mode != "traffic":
|
||||
if sale_base == "subscription":
|
||||
referral_bonus = await self.referral_service.apply_referral_bonuses_for_payment(
|
||||
session,
|
||||
payment.user_id,
|
||||
@@ -312,8 +313,8 @@ class FreeKassaService:
|
||||
config_link_display, connect_button_url = await prepare_config_links(self.settings, raw_config_link)
|
||||
config_link_text = config_link_display or _("config_link_not_available")
|
||||
final_end = activation.get("end_date") if activation else None
|
||||
months = payment.subscription_duration_months or 1
|
||||
sale_mode = "traffic" if self.settings.traffic_sale_mode else "subscription"
|
||||
months = payment.purchased_gb or payment.subscription_duration_months or 1
|
||||
sale_mode = payment.sale_mode or ("traffic" if self.settings.traffic_sale_mode else "subscription")
|
||||
|
||||
applied_days = 0
|
||||
if referral_bonus and referral_bonus.get("referee_new_end_date"):
|
||||
@@ -330,7 +331,7 @@ class FreeKassaService:
|
||||
|
||||
traffic_label = str(int(months)) if float(months).is_integer() else f"{months:g}"
|
||||
|
||||
if sale_mode == "traffic":
|
||||
if sale_mode.split("@", 1)[0].split("|", 1)[0] in {"traffic", "traffic_package", "topup"}:
|
||||
text = _("payment_successful_traffic_full",
|
||||
traffic_gb=traffic_label,
|
||||
end_date=end_date_str if final_end else "",
|
||||
|
||||
@@ -556,6 +556,48 @@ class PanelApiService:
|
||||
return response_data.get("response")
|
||||
return None
|
||||
|
||||
async def get_user_bandwidth_stats(self, user_uuid: str) -> Optional[Dict[str, Any]]:
|
||||
endpoint = f"/bandwidth-stats/users/{user_uuid}"
|
||||
response_data = await self._request("GET", endpoint, log_full_response=False)
|
||||
if response_data and not response_data.get("error") and "response" in response_data:
|
||||
return response_data.get("response")
|
||||
logging.error("Failed to get bandwidth stats for user %s. Response: %s", user_uuid, response_data)
|
||||
return None
|
||||
|
||||
async def reset_user_traffic(self, user_uuid: str) -> bool:
|
||||
endpoint = f"/users/{user_uuid}/actions/reset-traffic"
|
||||
response_data = await self._request("POST", endpoint, log_full_response=False)
|
||||
if response_data and not response_data.get("error"):
|
||||
return True
|
||||
logging.error("Failed to reset traffic for user %s. Response: %s", user_uuid, response_data)
|
||||
return False
|
||||
|
||||
async def add_users_to_internal_squad(self, squad_uuid: str, user_uuids: List[str]) -> bool:
|
||||
endpoint = f"/internal-squads/{squad_uuid}/bulk-actions/add-users"
|
||||
response_data = await self._request(
|
||||
"POST",
|
||||
endpoint,
|
||||
json={"users": user_uuids, "userUuids": user_uuids},
|
||||
log_full_response=False,
|
||||
)
|
||||
if response_data and not response_data.get("error"):
|
||||
return True
|
||||
logging.error("Failed to add users to squad %s. Response: %s", squad_uuid, response_data)
|
||||
return False
|
||||
|
||||
async def remove_users_from_internal_squad(self, squad_uuid: str, user_uuids: List[str]) -> bool:
|
||||
endpoint = f"/internal-squads/{squad_uuid}/bulk-actions/remove-users"
|
||||
response_data = await self._request(
|
||||
"DELETE",
|
||||
endpoint,
|
||||
json={"users": user_uuids, "userUuids": user_uuids},
|
||||
log_full_response=False,
|
||||
)
|
||||
if response_data and not response_data.get("error"):
|
||||
return True
|
||||
logging.error("Failed to remove users from squad %s. Response: %s", squad_uuid, response_data)
|
||||
return False
|
||||
|
||||
async def get_nodes_statistics(self) -> Optional[Dict[str, Any]]:
|
||||
"""Get nodes statistics"""
|
||||
response_data = await self._request("GET", "/system/stats/nodes", log_full_response=False)
|
||||
|
||||
@@ -179,8 +179,9 @@ class PlategaService:
|
||||
if payment.status == "succeeded" and status == "CONFIRMED":
|
||||
return web.Response(text="ok")
|
||||
|
||||
payment_months = payment.subscription_duration_months or 1
|
||||
sale_mode = "traffic" if self.settings.traffic_sale_mode else "subscription"
|
||||
payment_months = payment.purchased_gb or payment.subscription_duration_months or 1
|
||||
sale_mode = payment.sale_mode or ("traffic" if self.settings.traffic_sale_mode else "subscription")
|
||||
sale_base = sale_mode.split("@", 1)[0].split("|", 1)[0]
|
||||
|
||||
if status == "CONFIRMED":
|
||||
if amount_raw is not None:
|
||||
@@ -208,16 +209,16 @@ class PlategaService:
|
||||
activation = await self.subscription_service.activate_subscription(
|
||||
session,
|
||||
payment.user_id,
|
||||
int(payment_months) if sale_mode != "traffic" else 0,
|
||||
int(payment_months) if sale_base == "subscription" else int(float(payment_months)),
|
||||
float(payment.amount),
|
||||
payment.payment_id,
|
||||
provider="platega",
|
||||
sale_mode=sale_mode,
|
||||
traffic_gb=payment_months if sale_mode == "traffic" else None,
|
||||
traffic_gb=float(payment_months) if sale_base in {"traffic", "traffic_package", "topup"} else None,
|
||||
)
|
||||
|
||||
referral_bonus = None
|
||||
if sale_mode != "traffic":
|
||||
if sale_base == "subscription":
|
||||
referral_bonus = await self.referral_service.apply_referral_bonuses_for_payment(
|
||||
session,
|
||||
payment.user_id,
|
||||
@@ -249,7 +250,7 @@ class PlategaService:
|
||||
|
||||
traffic_label = str(int(payment_months)) if float(payment_months).is_integer() else f"{payment_months:g}"
|
||||
|
||||
if sale_mode == "traffic":
|
||||
if sale_base in {"traffic", "traffic_package", "topup"}:
|
||||
text = _(
|
||||
"payment_successful_traffic_full",
|
||||
traffic_gb=traffic_label,
|
||||
|
||||
@@ -190,8 +190,9 @@ class SeverPayService:
|
||||
logging.error("SeverPay webhook: payment not found (order_id=%s, provider_id=%s)", order_id_raw, provider_payment_id)
|
||||
return web.json_response({"status": False, "msg": "payment_not_found"}, status=404)
|
||||
|
||||
payment_months = payment.subscription_duration_months or 1
|
||||
sale_mode = "traffic" if self.settings.traffic_sale_mode else "subscription"
|
||||
payment_months = payment.purchased_gb or payment.subscription_duration_months or 1
|
||||
sale_mode = payment.sale_mode or ("traffic" if self.settings.traffic_sale_mode else "subscription")
|
||||
sale_base = sale_mode.split("@", 1)[0].split("|", 1)[0]
|
||||
if status == "success":
|
||||
try:
|
||||
await payment_dal.update_provider_payment_and_status(
|
||||
@@ -204,16 +205,16 @@ class SeverPayService:
|
||||
activation = await self.subscription_service.activate_subscription(
|
||||
session,
|
||||
payment.user_id,
|
||||
int(payment_months) if sale_mode != "traffic" else 0,
|
||||
int(payment_months) if sale_base == "subscription" else int(float(payment_months)),
|
||||
float(payment.amount),
|
||||
payment.payment_id,
|
||||
provider="severpay",
|
||||
sale_mode=sale_mode,
|
||||
traffic_gb=payment_months if sale_mode == "traffic" else None,
|
||||
traffic_gb=float(payment_months) if sale_base in {"traffic", "traffic_package", "topup"} else None,
|
||||
)
|
||||
|
||||
referral_bonus = None
|
||||
if sale_mode != "traffic":
|
||||
if sale_base == "subscription":
|
||||
referral_bonus = await self.referral_service.apply_referral_bonuses_for_payment(
|
||||
session,
|
||||
payment.user_id,
|
||||
@@ -245,7 +246,7 @@ class SeverPayService:
|
||||
|
||||
traffic_label = str(int(payment_months)) if float(payment_months).is_integer() else f"{payment_months:g}"
|
||||
|
||||
if sale_mode == "traffic":
|
||||
if sale_base in {"traffic", "traffic_package", "topup"}:
|
||||
text = _(
|
||||
"payment_successful_traffic_full",
|
||||
traffic_gb=traffic_label,
|
||||
|
||||
@@ -28,6 +28,7 @@ class StarsService:
|
||||
|
||||
async def create_invoice(self, session: AsyncSession, user_id: int, months: int,
|
||||
stars_price: int, description: str, sale_mode: str = "subscription") -> Optional[int]:
|
||||
sale_base = sale_mode.split("@", 1)[0].split("|", 1)[0]
|
||||
payment_record_data = {
|
||||
"user_id": user_id,
|
||||
"amount": float(stars_price),
|
||||
@@ -36,6 +37,9 @@ class StarsService:
|
||||
"description": description,
|
||||
"subscription_duration_months": int(months),
|
||||
"provider": "telegram_stars",
|
||||
"sale_mode": sale_mode,
|
||||
"tariff_key": sale_mode.split("@", 1)[1] if "@" in sale_mode else None,
|
||||
"purchased_gb": float(months) if sale_base in {"traffic", "traffic_package", "topup"} else None,
|
||||
}
|
||||
try:
|
||||
db_payment_record = await payment_dal.create_payment_record(
|
||||
@@ -85,15 +89,16 @@ class StarsService:
|
||||
exc_info=True)
|
||||
return
|
||||
|
||||
sale_base = sale_mode.split("@", 1)[0].split("|", 1)[0]
|
||||
activation_details = await self.subscription_service.activate_subscription(
|
||||
session,
|
||||
message.from_user.id,
|
||||
int(months) if sale_mode != "traffic" else 0,
|
||||
int(months) if sale_base == "subscription" else int(float(months)),
|
||||
float(stars_amount),
|
||||
payment_db_id,
|
||||
provider="telegram_stars",
|
||||
sale_mode=sale_mode,
|
||||
traffic_gb=months if sale_mode == "traffic" else None,
|
||||
traffic_gb=months if sale_base in {"traffic", "traffic_package", "topup"} else None,
|
||||
)
|
||||
if not activation_details or not activation_details.get("end_date"):
|
||||
logging.error(
|
||||
@@ -101,7 +106,7 @@ class StarsService:
|
||||
return
|
||||
|
||||
referral_bonus = None
|
||||
if sale_mode != "traffic":
|
||||
if sale_base == "subscription":
|
||||
referral_bonus = await self.referral_service.apply_referral_bonuses_for_payment(
|
||||
session,
|
||||
message.from_user.id,
|
||||
@@ -126,7 +131,7 @@ class StarsService:
|
||||
config_link_display, connect_button_url = await prepare_config_links(self.settings, raw_config_link)
|
||||
config_link_text = config_link_display or _("config_link_not_available")
|
||||
|
||||
if sale_mode == "traffic":
|
||||
if sale_base in {"traffic", "traffic_package", "topup"}:
|
||||
success_msg = _(
|
||||
"payment_successful_traffic_full",
|
||||
traffic_gb=str(int(months)) if float(months).is_integer() else f"{months:g}",
|
||||
@@ -188,10 +193,10 @@ class StarsService:
|
||||
user_id=message.from_user.id,
|
||||
amount=float(stars_amount),
|
||||
currency="XTR",
|
||||
months=int(months) if sale_mode != "traffic" else 0,
|
||||
months=int(months) if sale_base == "subscription" else 0,
|
||||
payment_provider="stars",
|
||||
username=user.username if user else None,
|
||||
traffic_gb=months if sale_mode == "traffic" else None,
|
||||
traffic_gb=months if sale_base in {"traffic", "traffic_package", "topup"} else None,
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to send stars payment notification: {e}")
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import logging
|
||||
import math
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional, Dict, Any, List, Tuple
|
||||
from aiogram import Bot
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
|
||||
from db.dal import user_dal, subscription_dal, promo_code_dal, payment_dal, user_billing_dal
|
||||
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.config_link import prepare_config_links
|
||||
from db.models import User, Subscription
|
||||
@@ -30,6 +32,72 @@ class SubscriptionService:
|
||||
self.bot = bot
|
||||
self.i18n = i18n
|
||||
|
||||
@staticmethod
|
||||
def gb_to_bytes(gb: float) -> int:
|
||||
return int(float(gb) * (1024**3))
|
||||
|
||||
@staticmethod
|
||||
def _far_future() -> datetime:
|
||||
return datetime(2099, 1, 1, tzinfo=timezone.utc)
|
||||
|
||||
def _parse_sale_mode_context(
|
||||
self,
|
||||
sale_mode: str,
|
||||
explicit_tariff_key: Optional[str] = None,
|
||||
) -> Tuple[str, Optional[str]]:
|
||||
mode = (sale_mode or "subscription").strip()
|
||||
tariff_key = explicit_tariff_key
|
||||
for separator in ("@", "|"):
|
||||
if separator in mode:
|
||||
base, suffix = mode.split(separator, 1)
|
||||
mode = base or mode
|
||||
tariff_key = tariff_key or suffix or None
|
||||
break
|
||||
return mode, tariff_key
|
||||
|
||||
def _tariffs_config(self):
|
||||
return getattr(self.settings, "tariffs_config", None)
|
||||
|
||||
def _default_tariff(self) -> Optional[Tariff]:
|
||||
config = self._tariffs_config()
|
||||
return config.default if config else None
|
||||
|
||||
def _resolve_tariff(self, tariff_key: Optional[str], billing_model: Optional[str] = None) -> Optional[Tariff]:
|
||||
config = self._tariffs_config()
|
||||
if not config:
|
||||
return None
|
||||
tariff = config.require(tariff_key or config.default_tariff)
|
||||
if billing_model and tariff.billing_model != billing_model:
|
||||
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]]:
|
||||
if tariff:
|
||||
return tariff.squad_uuids
|
||||
return self.settings.parsed_user_squad_uuids
|
||||
|
||||
def _traffic_limit_for_period_tariff(self, tariff: Optional[Tariff], topup_balance_bytes: int = 0) -> int:
|
||||
if tariff:
|
||||
return int(tariff.monthly_bytes + max(0, topup_balance_bytes))
|
||||
return self.settings.user_traffic_limit_bytes
|
||||
|
||||
async def _record_payment_context(
|
||||
self,
|
||||
session: AsyncSession,
|
||||
payment_db_id: int,
|
||||
*,
|
||||
sale_mode: str,
|
||||
tariff_key: Optional[str],
|
||||
purchased_gb: Optional[float] = None,
|
||||
) -> None:
|
||||
payment = await payment_dal.get_payment_by_db_id(session, payment_db_id)
|
||||
if not payment:
|
||||
return
|
||||
payment.sale_mode = sale_mode
|
||||
payment.tariff_key = tariff_key
|
||||
payment.purchased_gb = purchased_gb
|
||||
await session.flush()
|
||||
|
||||
async def get_user_language(self, session: AsyncSession, user_id: int) -> str:
|
||||
user_record = await user_dal.get_user_by_id(session, user_id)
|
||||
return (
|
||||
@@ -490,8 +558,18 @@ class SubscriptionService:
|
||||
payment_amount: float,
|
||||
payment_db_id: int,
|
||||
provider: str = "yookassa",
|
||||
tariff_key: Optional[str] = None,
|
||||
sale_mode: str = "traffic",
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Activate or extend a traffic-based package instead of a time-based subscription."""
|
||||
tariff = self._resolve_tariff(tariff_key, "traffic") if self._tariffs_config() else None
|
||||
await self._record_payment_context(
|
||||
session,
|
||||
payment_db_id,
|
||||
sale_mode=sale_mode,
|
||||
tariff_key=tariff.key if tariff else tariff_key,
|
||||
purchased_gb=float(traffic_gb),
|
||||
)
|
||||
db_user = await user_dal.get_user_by_id(session, user_id)
|
||||
if not db_user:
|
||||
logging.error("User %s not found for traffic package activation", user_id)
|
||||
@@ -516,12 +594,14 @@ class SubscriptionService:
|
||||
if current_used is None and active_sub:
|
||||
current_used = active_sub.traffic_used_bytes
|
||||
|
||||
purchase_bytes = int(float(traffic_gb) * (1024**3))
|
||||
new_limit = (current_limit or 0) + purchase_bytes
|
||||
purchase_bytes = self.gb_to_bytes(traffic_gb)
|
||||
remaining_bytes = max(0, int(current_limit or 0) - int(current_used or 0))
|
||||
new_balance = remaining_bytes + purchase_bytes
|
||||
new_limit = int(current_used or 0) + new_balance
|
||||
|
||||
start_date = datetime.now(timezone.utc)
|
||||
# Set a far-future expiry to satisfy panel requirements; keep the latest known expiry if it's further.
|
||||
far_future = datetime(2099, 1, 1, tzinfo=timezone.utc)
|
||||
far_future = self._far_future()
|
||||
final_end_date = far_future
|
||||
if active_sub and active_sub.end_date and active_sub.end_date > final_end_date:
|
||||
final_end_date = active_sub.end_date
|
||||
@@ -544,6 +624,12 @@ class SubscriptionService:
|
||||
"provider": provider,
|
||||
"skip_notifications": True,
|
||||
"auto_renew_enabled": False,
|
||||
"tariff_key": tariff.key if tariff else None,
|
||||
"tier_baseline_bytes": 0,
|
||||
"topup_balance_bytes": new_balance,
|
||||
"period_start_at": None,
|
||||
"is_throttled": False,
|
||||
"effective_monthly_price_rub": None,
|
||||
}
|
||||
|
||||
try:
|
||||
@@ -559,6 +645,8 @@ class SubscriptionService:
|
||||
traffic_limit_bytes=new_limit,
|
||||
traffic_limit_strategy="NO_RESET",
|
||||
)
|
||||
if tariff:
|
||||
panel_update_payload["activeInternalSquads"] = tariff.squad_uuids
|
||||
|
||||
panel_update_payload.update(self._panel_identity_payload_for_user(db_user))
|
||||
|
||||
@@ -575,6 +663,13 @@ class SubscriptionService:
|
||||
|
||||
final_subscription_url = updated_panel_user.get("subscriptionUrl")
|
||||
final_panel_short_uuid = updated_panel_user.get("shortUuid", panel_short_uuid)
|
||||
await tariff_dal.create_traffic_topup(
|
||||
session,
|
||||
subscription_id=new_or_updated_sub.subscription_id,
|
||||
payment_id=payment_db_id,
|
||||
purchased_bytes=purchase_bytes,
|
||||
kind="traffic_package",
|
||||
)
|
||||
|
||||
await self._send_payment_success_email(
|
||||
db_user=db_user,
|
||||
@@ -595,8 +690,204 @@ class SubscriptionService:
|
||||
"subscription_url": final_subscription_url,
|
||||
"applied_promo_bonus_days": 0,
|
||||
"traffic_limit_bytes": new_limit,
|
||||
"tariff_key": tariff.key if tariff else None,
|
||||
}
|
||||
|
||||
async def activate_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 tariff.billing_model == "traffic":
|
||||
return await self._activate_traffic_package(
|
||||
session=session,
|
||||
user_id=user_id,
|
||||
traffic_gb=traffic_gb,
|
||||
payment_amount=payment_amount,
|
||||
payment_db_id=payment_db_id,
|
||||
provider=provider,
|
||||
tariff_key=tariff.key,
|
||||
sale_mode="traffic_package",
|
||||
)
|
||||
|
||||
await self._record_payment_context(
|
||||
session,
|
||||
payment_db_id,
|
||||
sale_mode="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)
|
||||
new_topup_balance = int(sub.topup_balance_bytes or 0) + purchase_bytes
|
||||
new_limit = int(sub.tier_baseline_bytes or tariff.monthly_bytes) + new_topup_balance
|
||||
updated_sub = await subscription_dal.update_subscription(
|
||||
session,
|
||||
sub.subscription_id,
|
||||
{
|
||||
"topup_balance_bytes": new_topup_balance,
|
||||
"traffic_limit_bytes": new_limit,
|
||||
"is_throttled": False,
|
||||
"tariff_key": tariff.key,
|
||||
},
|
||||
)
|
||||
panel_payload = self._build_panel_update_payload(
|
||||
panel_user_uuid=db_user.panel_user_uuid,
|
||||
expire_at=updated_sub.end_date,
|
||||
status="ACTIVE",
|
||||
traffic_limit_bytes=new_limit,
|
||||
)
|
||||
panel_payload["activeInternalSquads"] = tariff.squad_uuids
|
||||
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(
|
||||
session,
|
||||
subscription_id=sub.subscription_id,
|
||||
payment_id=payment_db_id,
|
||||
purchased_bytes=purchase_bytes,
|
||||
kind="topup",
|
||||
)
|
||||
return {
|
||||
"subscription_id": sub.subscription_id,
|
||||
"traffic_limit_bytes": new_limit,
|
||||
"topup_balance_bytes": new_topup_balance,
|
||||
"tariff_key": tariff.key,
|
||||
}
|
||||
|
||||
def calculate_tariff_switch_options(self, sub: Subscription, target_tariff: Tariff) -> Dict[str, Any]:
|
||||
current_tariff = self._resolve_tariff(sub.tariff_key) if sub.tariff_key else self._default_tariff()
|
||||
now = datetime.now(timezone.utc)
|
||||
remaining_days = max(0, (sub.end_date - now).days) if sub.end_date else 0
|
||||
effective = float(sub.effective_monthly_price_rub or 0)
|
||||
current_model = current_tariff.billing_model if current_tariff else "period"
|
||||
|
||||
if current_model == "period" and target_tariff.billing_model == "period":
|
||||
target_monthly = target_tariff.period_price(1, "rub") or target_tariff.min_period_price_rub() or effective or 1
|
||||
remaining_value = remaining_days * (effective / 30) if effective else 0
|
||||
days_after = math.floor((remaining_value / float(target_monthly)) * 30) if target_monthly else remaining_days
|
||||
paid_diff = max(0, math.ceil((float(target_monthly) - effective) * remaining_days / 30)) if effective else 0
|
||||
return {
|
||||
"mode": "period_to_period",
|
||||
"remaining_days": remaining_days,
|
||||
"recalc_days": max(0, days_after),
|
||||
"paid_diff_rub": paid_diff,
|
||||
"target_monthly_rub": float(target_monthly),
|
||||
}
|
||||
|
||||
if current_model == "period" and target_tariff.billing_model == "traffic":
|
||||
rub_per_gb = target_tariff.rub_per_gb_for_conversion()
|
||||
remaining_value = remaining_days * (effective / 30) if effective else 0
|
||||
converted_gb = math.floor(remaining_value / rub_per_gb) if rub_per_gb else 0
|
||||
return {
|
||||
"mode": "period_to_traffic",
|
||||
"remaining_days": remaining_days,
|
||||
"converted_gb": max(0, converted_gb),
|
||||
"rub_per_gb": rub_per_gb,
|
||||
}
|
||||
|
||||
return {"mode": "traffic_to_period", "remaining_days": remaining_days}
|
||||
|
||||
async def switch_tariff_without_payment(
|
||||
self,
|
||||
session: AsyncSession,
|
||||
user_id: int,
|
||||
target_tariff_key: str,
|
||||
mode: str,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
config = self._tariffs_config()
|
||||
if not config:
|
||||
return None
|
||||
target = config.require(target_tariff_key)
|
||||
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
|
||||
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}
|
||||
converted_bytes = None
|
||||
|
||||
if target.billing_model == "period":
|
||||
update_data["tier_baseline_bytes"] = target.monthly_bytes
|
||||
update_data["traffic_limit_bytes"] = target.monthly_bytes + int(sub.topup_balance_bytes or 0)
|
||||
update_data["period_start_at"] = sub.period_start_at or now
|
||||
update_data["effective_monthly_price_rub"] = target.period_price(1, "rub") or target.min_period_price_rub()
|
||||
if mode == "recalc_days" and options.get("recalc_days") is not None:
|
||||
update_data["end_date"] = now + timedelta(days=int(options["recalc_days"]))
|
||||
else:
|
||||
converted_gb = float(options.get("converted_gb", 0))
|
||||
converted_bytes = self.gb_to_bytes(converted_gb)
|
||||
old_topup = int(sub.topup_balance_bytes or 0)
|
||||
new_balance = old_topup + converted_bytes
|
||||
panel_user = await self.panel_service.get_user_by_uuid(db_user.panel_user_uuid, log_response=False) or {}
|
||||
current_used, _, _ = self._extract_panel_traffic_details(panel_user)
|
||||
update_data.update(
|
||||
{
|
||||
"end_date": self._far_future(),
|
||||
"period_start_at": None,
|
||||
"tier_baseline_bytes": 0,
|
||||
"topup_balance_bytes": new_balance,
|
||||
"traffic_limit_bytes": int(current_used or 0) + new_balance,
|
||||
"traffic_used_bytes": current_used,
|
||||
"effective_monthly_price_rub": None,
|
||||
"auto_renew_enabled": False,
|
||||
"skip_notifications": True,
|
||||
}
|
||||
)
|
||||
|
||||
updated = await subscription_dal.update_subscription(session, sub.subscription_id, update_data)
|
||||
if not updated:
|
||||
return None
|
||||
panel_payload = self._build_panel_update_payload(
|
||||
panel_user_uuid=db_user.panel_user_uuid,
|
||||
expire_at=updated.end_date,
|
||||
status="ACTIVE",
|
||||
traffic_limit_bytes=updated.traffic_limit_bytes,
|
||||
traffic_limit_strategy="NO_RESET" if target.billing_model == "traffic" else self.settings.USER_TRAFFIC_STRATEGY,
|
||||
)
|
||||
panel_payload["activeInternalSquads"] = target.squad_uuids
|
||||
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:
|
||||
await tariff_dal.create_traffic_topup(
|
||||
session,
|
||||
subscription_id=updated.subscription_id,
|
||||
payment_id=None,
|
||||
purchased_bytes=converted_bytes,
|
||||
kind="conversion",
|
||||
)
|
||||
await tariff_dal.create_tariff_change(
|
||||
session,
|
||||
{
|
||||
"subscription_id": updated.subscription_id,
|
||||
"from_tariff_key": before_tariff_key,
|
||||
"to_tariff_key": target.key,
|
||||
"mode": mode,
|
||||
"payment_id": None,
|
||||
"days_before": options.get("remaining_days"),
|
||||
"days_after": (updated.end_date - now).days if updated.end_date and target.billing_model == "period" else None,
|
||||
"converted_bytes": converted_bytes,
|
||||
"eff_price_before": sub.effective_monthly_price_rub,
|
||||
"eff_price_after": updated.effective_monthly_price_rub,
|
||||
},
|
||||
)
|
||||
return {"subscription_id": updated.subscription_id, "tariff_key": target.key}
|
||||
|
||||
async def activate_subscription(
|
||||
self,
|
||||
session: AsyncSession,
|
||||
@@ -608,9 +899,14 @@ class SubscriptionService:
|
||||
provider: str = "yookassa",
|
||||
sale_mode: str = "subscription",
|
||||
traffic_gb: Optional[float] = None,
|
||||
tariff_key: Optional[str] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
|
||||
if sale_mode == "traffic" or getattr(self.settings, "traffic_sale_mode", False):
|
||||
sale_mode_base, sale_mode_tariff_key = self._parse_sale_mode_context(sale_mode, tariff_key)
|
||||
tariff_key = sale_mode_tariff_key
|
||||
if sale_mode_base in {"traffic", "traffic_package"} or (
|
||||
getattr(self.settings, "traffic_sale_mode", False) and not self._tariffs_config()
|
||||
):
|
||||
target_gb = traffic_gb if traffic_gb is not None else float(months)
|
||||
return await self._activate_traffic_package(
|
||||
session=session,
|
||||
@@ -619,7 +915,79 @@ class SubscriptionService:
|
||||
payment_amount=payment_amount,
|
||||
payment_db_id=payment_db_id,
|
||||
provider=provider,
|
||||
tariff_key=tariff_key,
|
||||
sale_mode="traffic_package" if self._tariffs_config() else "traffic",
|
||||
)
|
||||
if sale_mode_base == "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("Top-up activation requires tariff_key for user %s", user_id)
|
||||
return None
|
||||
return await self.activate_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 == "tariff_upgrade":
|
||||
if not tariff_key:
|
||||
logging.error("Tariff upgrade activation requires tariff_key for user %s", user_id)
|
||||
return None
|
||||
await self._record_payment_context(
|
||||
session,
|
||||
payment_db_id,
|
||||
sale_mode="tariff_upgrade",
|
||||
tariff_key=tariff_key,
|
||||
purchased_gb=None,
|
||||
)
|
||||
result = await self.switch_tariff_without_payment(
|
||||
session,
|
||||
user_id,
|
||||
tariff_key,
|
||||
"paid_diff",
|
||||
)
|
||||
if result:
|
||||
sub = await subscription_dal.get_active_subscription_by_user_id(session, user_id)
|
||||
if sub:
|
||||
await tariff_dal.create_tariff_change(
|
||||
session,
|
||||
{
|
||||
"subscription_id": sub.subscription_id,
|
||||
"from_tariff_key": None,
|
||||
"to_tariff_key": tariff_key,
|
||||
"mode": "paid_diff",
|
||||
"payment_id": payment_db_id,
|
||||
"days_before": None,
|
||||
"days_after": (sub.end_date - datetime.now(timezone.utc)).days if sub.end_date else None,
|
||||
"converted_bytes": None,
|
||||
"eff_price_before": None,
|
||||
"eff_price_after": sub.effective_monthly_price_rub,
|
||||
},
|
||||
)
|
||||
result["end_date"] = sub.end_date
|
||||
result["is_active"] = sub.is_active
|
||||
return result
|
||||
|
||||
tariff = self._resolve_tariff(tariff_key, "period") if self._tariffs_config() else None
|
||||
await self._record_payment_context(
|
||||
session,
|
||||
payment_db_id,
|
||||
sale_mode=sale_mode_base,
|
||||
tariff_key=tariff.key if tariff else tariff_key,
|
||||
purchased_gb=None,
|
||||
)
|
||||
|
||||
db_user = await user_dal.get_user_by_id(session, user_id)
|
||||
if not db_user:
|
||||
@@ -647,6 +1015,11 @@ class SubscriptionService:
|
||||
session, user_id, panel_user_uuid
|
||||
)
|
||||
start_date = datetime.now(timezone.utc)
|
||||
starts_after_lapse = not (
|
||||
current_active_sub
|
||||
and current_active_sub.end_date
|
||||
and current_active_sub.end_date > start_date
|
||||
)
|
||||
if (
|
||||
current_active_sub
|
||||
and current_active_sub.end_date
|
||||
@@ -702,6 +1075,15 @@ class SubscriptionService:
|
||||
session, user_id
|
||||
)
|
||||
|
||||
topup_balance_bytes = int(getattr(current_active_sub, "topup_balance_bytes", 0) or 0)
|
||||
tier_baseline_bytes = tariff.monthly_bytes if tariff else self.settings.user_traffic_limit_bytes
|
||||
period_start_at = (
|
||||
datetime.now(timezone.utc)
|
||||
if starts_after_lapse or not current_active_sub or not getattr(current_active_sub, "period_start_at", None)
|
||||
else current_active_sub.period_start_at
|
||||
)
|
||||
effective_monthly_price = float(payment_amount) / max(1, months_int)
|
||||
traffic_limit_bytes = self._traffic_limit_for_period_tariff(tariff, topup_balance_bytes)
|
||||
sub_payload = {
|
||||
"user_id": user_id,
|
||||
"panel_user_uuid": panel_user_uuid,
|
||||
@@ -711,10 +1093,16 @@ class SubscriptionService:
|
||||
"duration_months": months_int,
|
||||
"is_active": True,
|
||||
"status_from_panel": "ACTIVE",
|
||||
"traffic_limit_bytes": self.settings.user_traffic_limit_bytes,
|
||||
"traffic_limit_bytes": traffic_limit_bytes,
|
||||
"provider": provider,
|
||||
"skip_notifications": False,
|
||||
"auto_renew_enabled": auto_renew_should_enable,
|
||||
"tariff_key": tariff.key if tariff else None,
|
||||
"tier_baseline_bytes": tier_baseline_bytes,
|
||||
"topup_balance_bytes": topup_balance_bytes,
|
||||
"period_start_at": period_start_at,
|
||||
"is_throttled": False,
|
||||
"effective_monthly_price_rub": effective_monthly_price,
|
||||
}
|
||||
try:
|
||||
new_or_updated_sub = await subscription_dal.upsert_subscription(
|
||||
@@ -731,8 +1119,10 @@ class SubscriptionService:
|
||||
panel_user_uuid=panel_user_uuid,
|
||||
expire_at=final_end_date,
|
||||
status="ACTIVE",
|
||||
traffic_limit_bytes=self.settings.user_traffic_limit_bytes,
|
||||
traffic_limit_bytes=traffic_limit_bytes,
|
||||
)
|
||||
if tariff:
|
||||
panel_update_payload["activeInternalSquads"] = tariff.squad_uuids
|
||||
|
||||
panel_update_payload.update(self._panel_identity_payload_for_user(db_user))
|
||||
|
||||
@@ -766,6 +1156,7 @@ class SubscriptionService:
|
||||
"panel_short_uuid": final_panel_short_uuid,
|
||||
"subscription_url": final_subscription_url,
|
||||
"applied_promo_bonus_days": applied_promo_bonus_days,
|
||||
"tariff_key": tariff.key if tariff else None,
|
||||
}
|
||||
|
||||
async def extend_active_subscription_days(
|
||||
@@ -983,6 +1374,12 @@ class SubscriptionService:
|
||||
hwid_limit = panel_user_data.get("hwidDeviceLimit")
|
||||
if hwid_limit is None:
|
||||
hwid_limit = self.settings.USER_HWID_DEVICE_LIMIT
|
||||
tariff = None
|
||||
if local_active_sub and local_active_sub.tariff_key and self._tariffs_config():
|
||||
try:
|
||||
tariff = self._resolve_tariff(local_active_sub.tariff_key)
|
||||
except Exception:
|
||||
tariff = None
|
||||
|
||||
return {
|
||||
"user_id": panel_user_data.get("uuid"),
|
||||
@@ -993,6 +1390,14 @@ class SubscriptionService:
|
||||
"traffic_limit_bytes": panel_traffic_limit,
|
||||
"traffic_used_bytes": panel_traffic_used,
|
||||
"traffic_limit_strategy": panel_traffic_strategy,
|
||||
"tariff_key": local_active_sub.tariff_key if local_active_sub else None,
|
||||
"tariff_name": tariff.name(db_user.language_code or self.settings.DEFAULT_LANGUAGE) if tariff else None,
|
||||
"tariff_description": tariff.description(db_user.language_code or self.settings.DEFAULT_LANGUAGE) if tariff else None,
|
||||
"billing_model": tariff.billing_model if tariff else ("traffic" if getattr(self.settings, "traffic_sale_mode", False) else "period"),
|
||||
"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,
|
||||
"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,
|
||||
"user_bot_username": db_user.username,
|
||||
"is_panel_data": True,
|
||||
"max_devices": hwid_limit,
|
||||
|
||||
Reference in New Issue
Block a user