Merge pull request #1028 from Fr1ngg/7c9pa7-bedolaga/add-subscription-settings-endpoints

Add miniapp subscription settings management endpoints
This commit is contained in:
Egor
2025-10-10 06:27:44 +03:00
committed by GitHub
2 changed files with 995 additions and 3 deletions

View File

@@ -24,9 +24,15 @@ from app.database.crud.discount_offer import (
from app.database.crud.promo_group import get_auto_assign_promo_groups
from app.database.crud.rules import get_rules_by_language
from app.database.crud.promo_offer_template import get_promo_offer_template_by_id
from app.database.crud.server_squad import get_server_squad_by_uuid
from app.database.crud.transaction import get_user_total_spent_kopeks
from app.database.crud.user import get_user_by_telegram_id
from app.database.crud.server_squad import (
get_available_server_squads,
get_server_squad_by_uuid,
)
from app.database.crud.transaction import (
create_transaction,
get_user_total_spent_kopeks,
)
from app.database.crud.user import get_user_by_telegram_id, subtract_user_balance
from app.database.models import (
PromoGroup,
PromoOfferTemplate,
@@ -50,6 +56,11 @@ from app.services.promocode_service import PromoCodeService
from app.services.subscription_service import SubscriptionService
from app.services.tribute_service import TributeService
from app.utils.currency_converter import currency_converter
from app.utils.pricing_utils import (
apply_percentage_discount,
calculate_prorated_price,
get_remaining_months,
)
from app.utils.subscription_utils import get_happ_cryptolink_redirect_link
from app.utils.telegram_webapp import (
TelegramWebAppAuthError,
@@ -96,6 +107,20 @@ from ..schemas.miniapp import (
MiniAppSubscriptionRequest,
MiniAppSubscriptionResponse,
MiniAppSubscriptionUser,
MiniAppSubscriptionSettingsActionResponse,
MiniAppSubscriptionSettingsCurrent,
MiniAppSubscriptionSettingsData,
MiniAppSubscriptionSettingsDevices,
MiniAppSubscriptionSettingsDeviceOption,
MiniAppSubscriptionSettingsRequest,
MiniAppSubscriptionSettingsResponse,
MiniAppSubscriptionSettingsServer,
MiniAppSubscriptionSettingsServers,
MiniAppSubscriptionSettingsTraffic,
MiniAppSubscriptionSettingsTrafficOption,
MiniAppSubscriptionServersUpdateRequest,
MiniAppSubscriptionTrafficUpdateRequest,
MiniAppSubscriptionDevicesUpdateRequest,
MiniAppTransaction,
)
@@ -198,6 +223,327 @@ def _normalize_stars_amount(amount_kopeks: int) -> Tuple[int, int]:
return stars_amount, normalized_amount_kopeks
def _get_period_hint_from_subscription(
subscription: Optional[Subscription],
) -> Optional[int]:
if not subscription or not getattr(subscription, "end_date", None):
return None
months_remaining = get_remaining_months(subscription.end_date)
if months_remaining <= 0:
return None
return months_remaining * 30
def _get_addon_discount_percent_for_user(
user: Optional[User],
category: str,
period_days_hint: Optional[int] = None,
) -> int:
if user is None:
return 0
promo_group = getattr(user, "promo_group", None)
if promo_group is None:
return 0
if not getattr(promo_group, "apply_discounts_to_addons", True):
return 0
try:
return int(user.get_promo_discount(category, period_days_hint) or 0)
except AttributeError:
return 0
def _format_total_price_label(amount_kopeks: int, months: int) -> Optional[str]:
if amount_kopeks <= 0:
return None
base_label = settings.format_price(amount_kopeks)
if months > 1:
return f"{base_label} (за {months} мес)"
return base_label
async def _authorize_subscription_user(
db: AsyncSession,
init_data: str,
subscription_id: Optional[int] = None,
) -> Tuple[User, Subscription]:
if not init_data:
raise HTTPException(
status.HTTP_401_UNAUTHORIZED,
detail={"message": "Authorization required"},
)
try:
webapp_data = parse_webapp_init_data(init_data, settings.BOT_TOKEN)
except TelegramWebAppAuthError as error:
raise HTTPException(
status.HTTP_401_UNAUTHORIZED,
detail={"message": str(error)},
) from error
telegram_user = webapp_data.get("user")
if not isinstance(telegram_user, dict) or "id" not in telegram_user:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"message": "Invalid Telegram user payload"},
)
try:
telegram_id = int(telegram_user["id"])
except (TypeError, ValueError):
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"message": "Invalid Telegram user identifier"},
) from None
user = await get_user_by_telegram_id(db, telegram_id)
if not user or not user.subscription:
raise HTTPException(
status.HTTP_404_NOT_FOUND,
detail={"message": "Subscription not found"},
)
subscription = user.subscription
if subscription_id and subscription.id != subscription_id:
raise HTTPException(
status.HTTP_404_NOT_FOUND,
detail={"message": "Subscription not found"},
)
if subscription.is_trial:
raise HTTPException(
status.HTTP_403_FORBIDDEN,
detail={"message": "Настройки доступны только для платных подписок"},
)
return user, subscription
async def _build_subscription_settings_payload(
db: AsyncSession,
user: User,
subscription: Subscription,
) -> MiniAppSubscriptionSettingsData:
period_hint_days = _get_period_hint_from_subscription(subscription)
servers_discount_percent = _get_addon_discount_percent_for_user(
user,
"servers",
period_hint_days,
)
traffic_discount_percent = _get_addon_discount_percent_for_user(
user,
"traffic",
period_hint_days,
)
devices_discount_percent = _get_addon_discount_percent_for_user(
user,
"devices",
period_hint_days,
)
connected_squads: List[str] = list(subscription.connected_squads or [])
available_servers = await get_available_server_squads(
db,
promo_group_id=getattr(user, "promo_group_id", None),
)
available_servers_map = {server.squad_uuid: server for server in available_servers}
connected_servers: List[MiniAppConnectedServer] = []
for squad_uuid in connected_squads:
server = available_servers_map.get(squad_uuid)
if not server:
server = await get_server_squad_by_uuid(db, squad_uuid)
name = getattr(server, "display_name", None) or squad_uuid
connected_servers.append(
MiniAppConnectedServer(
uuid=squad_uuid,
name=name,
)
)
months_to_charge = get_remaining_months(subscription.end_date)
available_payload: List[MiniAppSubscriptionSettingsServer] = []
seen_server_ids: set[str] = set()
for server in available_servers:
discounted_per_month, discount_per_month = apply_percentage_discount(
server.price_kopeks,
servers_discount_percent,
)
total_price, charged_months = calculate_prorated_price(
discounted_per_month,
subscription.end_date,
)
price_label = _format_total_price_label(total_price, charged_months)
is_available = server.is_available and not server.is_full
disabled_reason = None
if not server.is_available:
disabled_reason = "Server is unavailable"
elif server.is_full:
disabled_reason = "Server capacity reached"
available_payload.append(
MiniAppSubscriptionSettingsServer(
uuid=server.squad_uuid,
name=server.display_name,
price_kopeks=total_price,
price_label=price_label,
discount_percent=max(0, servers_discount_percent),
is_connected=server.squad_uuid in connected_squads,
is_available=is_available,
disabled_reason=disabled_reason,
)
)
seen_server_ids.add(server.squad_uuid)
for squad_uuid in connected_squads:
if squad_uuid in seen_server_ids:
continue
server = await get_server_squad_by_uuid(db, squad_uuid)
name = getattr(server, "display_name", None) or squad_uuid
available_payload.append(
MiniAppSubscriptionSettingsServer(
uuid=squad_uuid,
name=name,
price_kopeks=0,
discount_percent=max(0, servers_discount_percent),
is_connected=True,
is_available=False,
disabled_reason="Server is not available",
)
)
traffic_packages = [
package
for package in settings.get_traffic_packages()
if package.get("enabled", True)
]
current_traffic = subscription.traffic_limit_gb or 0
current_traffic_price = settings.get_traffic_price(current_traffic)
discounted_current_per_month, _ = apply_percentage_discount(
current_traffic_price,
traffic_discount_percent,
)
traffic_options: List[MiniAppSubscriptionSettingsTrafficOption] = []
for package in traffic_packages:
gb = int(package.get("gb", 0) or 0)
price_per_month = int(package.get("price", 0) or 0)
discounted_per_month, _ = apply_percentage_discount(
price_per_month,
traffic_discount_percent,
)
price_difference_per_month = discounted_per_month - discounted_current_per_month
total_price_difference = price_difference_per_month * months_to_charge
description = None
if price_difference_per_month < 0:
total_price_difference = 0
description = "Без возврата средств"
price_label = _format_total_price_label(
total_price_difference,
months_to_charge,
)
traffic_options.append(
MiniAppSubscriptionSettingsTrafficOption(
value=gb,
label="",
price_kopeks=max(0, total_price_difference),
price_label=price_label,
is_current=gb == current_traffic,
is_available=True,
description=description,
)
)
current_devices = subscription.device_limit or settings.DEFAULT_DEVICE_LIMIT
max_devices = settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else 0
device_options: List[MiniAppSubscriptionSettingsDeviceOption] = []
range_max = max_devices or max(current_devices + 5, settings.DEFAULT_DEVICE_LIMIT + 5)
for devices_count in range(1, range_max + 1):
current_chargeable = max(0, current_devices - settings.DEFAULT_DEVICE_LIMIT)
new_chargeable = max(0, devices_count - settings.DEFAULT_DEVICE_LIMIT)
chargeable_devices = new_chargeable - current_chargeable
if chargeable_devices > 0:
devices_price_per_month = chargeable_devices * settings.PRICE_PER_DEVICE
discounted_per_month, discount_per_month = apply_percentage_discount(
devices_price_per_month,
devices_discount_percent,
)
total_price, charged_months = calculate_prorated_price(
discounted_per_month,
subscription.end_date,
)
price_label = _format_total_price_label(total_price, charged_months)
else:
total_price = 0
price_label = None
device_options.append(
MiniAppSubscriptionSettingsDeviceOption(
value=devices_count,
label=str(devices_count),
price_kopeks=max(0, total_price),
price_label=price_label,
)
)
balance_currency = getattr(user, "balance_currency", None)
if isinstance(balance_currency, str) and balance_currency.strip():
currency = balance_currency.strip().upper()
else:
currency = "RUB"
current_payload = MiniAppSubscriptionSettingsCurrent(
servers=connected_servers,
traffic_limit_gb=subscription.traffic_limit_gb,
traffic_limit_label=_format_limit_label(subscription.traffic_limit_gb),
device_limit=subscription.device_limit,
)
servers_payload = MiniAppSubscriptionSettingsServers(
available=available_payload,
min=1,
max=0,
can_update=True,
hint=None,
)
traffic_payload = MiniAppSubscriptionSettingsTraffic(
options=traffic_options,
can_update=not settings.is_traffic_fixed(),
current_value=current_traffic,
)
devices_payload = MiniAppSubscriptionSettingsDevices(
options=device_options,
can_update=True,
min=1,
max=max_devices,
step=1,
current=current_devices,
price_kopeks=settings.PRICE_PER_DEVICE * months_to_charge,
)
return MiniAppSubscriptionSettingsData(
subscription_id=subscription.id,
currency=currency,
current=current_payload,
servers=servers_payload,
traffic=traffic_payload,
devices=devices_payload,
)
def _build_balance_invoice_payload(user_id: int, amount_kopeks: int) -> str:
suffix = uuid4().hex[:8]
return f"balance_{user_id}_{amount_kopeks}_{suffix}"
@@ -1958,6 +2304,546 @@ async def _build_referral_info(
)
@router.post(
"/subscription/settings",
response_model=MiniAppSubscriptionSettingsResponse,
)
async def get_subscription_settings(
payload: MiniAppSubscriptionSettingsRequest,
db: AsyncSession = Depends(get_db_session),
) -> MiniAppSubscriptionSettingsResponse:
user, subscription = await _authorize_subscription_user(
db,
payload.init_data,
payload.subscription_id,
)
settings_payload = await _build_subscription_settings_payload(
db,
user,
subscription,
)
return MiniAppSubscriptionSettingsResponse(settings=settings_payload)
@router.post(
"/subscription/servers",
response_model=MiniAppSubscriptionSettingsActionResponse,
)
async def update_subscription_servers(
payload: MiniAppSubscriptionServersUpdateRequest,
db: AsyncSession = Depends(get_db_session),
) -> MiniAppSubscriptionSettingsActionResponse:
user, subscription = await _authorize_subscription_user(
db,
payload.init_data,
payload.subscription_id,
)
selection_sources = [
payload.squads,
payload.servers,
payload.squad_uuids,
payload.server_uuids,
]
selected_order: List[str] = []
for source in selection_sources:
for value in source or []:
normalized = (value or "").strip()
if normalized and normalized not in selected_order:
selected_order.append(normalized)
if not selected_order:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"message": "Необходимо выбрать хотя бы один сервер"},
)
current_set = set(subscription.connected_squads or [])
if len(selected_order) < 1:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"message": "Нужно выбрать хотя бы один сервер"},
)
available_servers = await get_available_server_squads(
db,
promo_group_id=getattr(user, "promo_group_id", None),
)
available_map = {server.squad_uuid: server for server in available_servers}
allowed_uuids = set(available_map.keys()) | current_set
for squad_uuid in selected_order:
if squad_uuid not in allowed_uuids:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"message": "Выбран недоступный сервер"},
)
if squad_uuid not in current_set:
server = available_map.get(squad_uuid)
if not server:
server = await get_server_squad_by_uuid(db, squad_uuid)
if not server:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"message": "Выбран недоступный сервер"},
)
available_map[squad_uuid] = server
if not (server.is_available and not server.is_full):
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"message": "Сервер временно недоступен"},
)
new_servers = [uuid for uuid in selected_order if uuid not in current_set]
removed_servers = [uuid for uuid in current_set if uuid not in selected_order]
if not new_servers and not removed_servers:
return MiniAppSubscriptionSettingsActionResponse(
success=True,
message="Изменений не обнаружено",
balance_kopeks=user.balance_kopeks,
charged_kopeks=0,
)
period_hint_days = _get_period_hint_from_subscription(subscription)
servers_discount_percent = _get_addon_discount_percent_for_user(
user,
"servers",
period_hint_days,
)
total_price = 0
total_discount = 0
new_server_names: List[str] = []
for squad_uuid in new_servers:
server = available_map.get(squad_uuid)
if not server:
server = await get_server_squad_by_uuid(db, squad_uuid)
if not server:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"message": "Сервер недоступен"},
)
discounted_per_month, discount_per_month = apply_percentage_discount(
server.price_kopeks,
servers_discount_percent,
)
charged_price, charged_months = calculate_prorated_price(
discounted_per_month,
subscription.end_date,
)
total_price += charged_price
total_discount += discount_per_month * charged_months
new_server_names.append(server.display_name or squad_uuid)
if total_price > 0 and user.balance_kopeks < total_price:
missing = total_price - user.balance_kopeks
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={
"code": "insufficient_funds",
"message": "Недостаточно средств на балансе",
"required_amount": total_price,
"missing_amount": missing,
"balance": user.balance_kopeks,
},
)
charged_amount = total_price
if total_price > 0:
description = "Добавление серверов к подписке"
if new_server_names:
description = (
"Добавление серверов к подписке: "
+ ", ".join(new_server_names)
)
success = await subtract_user_balance(
db,
user,
total_price,
description,
payment_method=PaymentMethod.BALANCE,
)
if not success:
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
detail={"message": "Не удалось списать средства"},
)
await create_transaction(
db=db,
user_id=user.id,
type=TransactionType.SUBSCRIPTION_PAYMENT,
amount_kopeks=total_price,
description=description,
payment_method=PaymentMethod.BALANCE,
)
subscription.connected_squads = selected_order
subscription.updated_at = datetime.utcnow()
await db.commit()
subscription_service = SubscriptionService()
try:
await subscription_service.update_remnawave_user(db, subscription)
except Exception as error:
logger.warning("Failed to update RemnaWave user after servers update: %s", error)
await db.refresh(user)
await db.refresh(subscription)
removed_names: List[str] = []
for squad_uuid in removed_servers:
server = available_map.get(squad_uuid)
if not server:
server = await get_server_squad_by_uuid(db, squad_uuid)
removed_names.append(getattr(server, "display_name", None) or squad_uuid)
message_parts: List[str] = []
if new_server_names:
message_parts.append(
"Добавлены серверы: " + ", ".join(new_server_names)
)
if total_discount > 0 and servers_discount_percent > 0:
message_parts.append(
(
"Применена скидка {percent}%: -{amount}".format(
percent=servers_discount_percent,
amount=settings.format_price(total_discount),
)
)
)
if removed_names:
message_parts.append(
"Отключены серверы: " + ", ".join(removed_names)
)
message = "\n".join(message_parts) if message_parts else "Настройки серверов обновлены"
return MiniAppSubscriptionSettingsActionResponse(
success=True,
message=message,
balance_kopeks=user.balance_kopeks,
charged_kopeks=charged_amount,
)
@router.post(
"/subscription/traffic",
response_model=MiniAppSubscriptionSettingsActionResponse,
)
async def update_subscription_traffic(
payload: MiniAppSubscriptionTrafficUpdateRequest,
db: AsyncSession = Depends(get_db_session),
) -> MiniAppSubscriptionSettingsActionResponse:
if settings.is_traffic_fixed():
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"message": "Изменение лимита трафика недоступно"},
)
user, subscription = await _authorize_subscription_user(
db,
payload.init_data,
payload.subscription_id,
)
new_traffic = (
payload.traffic
if payload.traffic is not None
else payload.traffic_gb
)
if new_traffic is None:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"message": "Не указан новый лимит трафика"},
)
try:
new_traffic_gb = int(new_traffic)
except (TypeError, ValueError):
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"message": "Некорректный лимит трафика"},
) from None
if new_traffic_gb < 0:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"message": "Некорректный лимит трафика"},
)
current_traffic = subscription.traffic_limit_gb or 0
if new_traffic_gb == current_traffic:
return MiniAppSubscriptionSettingsActionResponse(
success=True,
message="Лимит трафика не изменился",
balance_kopeks=user.balance_kopeks,
charged_kopeks=0,
)
old_price_per_month = settings.get_traffic_price(current_traffic)
new_price_per_month = settings.get_traffic_price(new_traffic_gb)
months_remaining = get_remaining_months(subscription.end_date)
period_hint_days = months_remaining * 30 if months_remaining > 0 else None
traffic_discount_percent = _get_addon_discount_percent_for_user(
user,
"traffic",
period_hint_days,
)
discounted_old_per_month, _ = apply_percentage_discount(
old_price_per_month,
traffic_discount_percent,
)
discounted_new_per_month, _ = apply_percentage_discount(
new_price_per_month,
traffic_discount_percent,
)
price_difference_per_month = discounted_new_per_month - discounted_old_per_month
total_price_difference = (
price_difference_per_month * months_remaining
if price_difference_per_month > 0
else 0
)
if total_price_difference > 0 and user.balance_kopeks < total_price_difference:
missing = total_price_difference - user.balance_kopeks
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={
"code": "insufficient_funds",
"message": "Недостаточно средств на балансе",
"required_amount": total_price_difference,
"missing_amount": missing,
"balance": user.balance_kopeks,
},
)
charged_amount = total_price_difference
if total_price_difference > 0:
description = (
f"Переключение трафика с {current_traffic}GB на {new_traffic_gb}GB"
)
success = await subtract_user_balance(
db,
user,
total_price_difference,
description,
payment_method=PaymentMethod.BALANCE,
)
if not success:
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
detail={"message": "Не удалось списать средства"},
)
await create_transaction(
db=db,
user_id=user.id,
type=TransactionType.SUBSCRIPTION_PAYMENT,
amount_kopeks=total_price_difference,
description=(
f"Переключение трафика с {current_traffic}GB на {new_traffic_gb}GB"
),
payment_method=PaymentMethod.BALANCE,
)
subscription.traffic_limit_gb = new_traffic_gb
subscription.updated_at = datetime.utcnow()
await db.commit()
subscription_service = SubscriptionService()
try:
await subscription_service.update_remnawave_user(db, subscription)
except Exception as error:
logger.warning(
"Failed to update RemnaWave user after traffic update: %s",
error,
)
await db.refresh(user)
await db.refresh(subscription)
message = (
f"Лимит трафика обновлен: {current_traffic}{new_traffic_gb}"
)
return MiniAppSubscriptionSettingsActionResponse(
success=True,
message=message,
balance_kopeks=user.balance_kopeks,
charged_kopeks=charged_amount,
)
@router.post(
"/subscription/devices",
response_model=MiniAppSubscriptionSettingsActionResponse,
)
async def update_subscription_devices(
payload: MiniAppSubscriptionDevicesUpdateRequest,
db: AsyncSession = Depends(get_db_session),
) -> MiniAppSubscriptionSettingsActionResponse:
user, subscription = await _authorize_subscription_user(
db,
payload.init_data,
payload.subscription_id,
)
if payload.devices is None:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"message": "Не указано новое количество устройств"},
)
try:
new_devices_count = int(payload.devices)
except (TypeError, ValueError):
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"message": "Некорректное количество устройств"},
) from None
if new_devices_count < 1:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"message": "Количество устройств должно быть не меньше 1"},
)
max_devices = settings.MAX_DEVICES_LIMIT
if max_devices > 0 and new_devices_count > max_devices:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={
"message": (
f"Превышен максимальный лимит устройств ({max_devices})"
)
},
)
current_devices = subscription.device_limit or settings.DEFAULT_DEVICE_LIMIT
if new_devices_count == current_devices:
return MiniAppSubscriptionSettingsActionResponse(
success=True,
message="Количество устройств не изменилось",
balance_kopeks=user.balance_kopeks,
charged_kopeks=0,
)
devices_difference = new_devices_count - current_devices
price = 0
charged_months = 0
if devices_difference > 0:
current_chargeable = max(0, current_devices - settings.DEFAULT_DEVICE_LIMIT)
new_chargeable = max(0, new_devices_count - settings.DEFAULT_DEVICE_LIMIT)
chargeable_devices = new_chargeable - current_chargeable
if chargeable_devices > 0:
devices_price_per_month = chargeable_devices * settings.PRICE_PER_DEVICE
months_hint = get_remaining_months(subscription.end_date)
period_hint_days = months_hint * 30 if months_hint > 0 else None
devices_discount_percent = _get_addon_discount_percent_for_user(
user,
"devices",
period_hint_days,
)
discounted_per_month, discount_per_month = apply_percentage_discount(
devices_price_per_month,
devices_discount_percent,
)
price, charged_months = calculate_prorated_price(
discounted_per_month,
subscription.end_date,
)
else:
price = 0
else:
price = 0
if price > 0 and user.balance_kopeks < price:
missing = price - user.balance_kopeks
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={
"code": "insufficient_funds",
"message": "Недостаточно средств на балансе",
"required_amount": price,
"missing_amount": missing,
"balance": user.balance_kopeks,
},
)
charged_amount = price
if price > 0:
description = (
f"Изменение устройств с {current_devices} до {new_devices_count}"
)
success = await subtract_user_balance(
db,
user,
price,
description,
payment_method=PaymentMethod.BALANCE,
)
if not success:
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
detail={"message": "Не удалось списать средства"},
)
await create_transaction(
db=db,
user_id=user.id,
type=TransactionType.SUBSCRIPTION_PAYMENT,
amount_kopeks=price,
description=(
f"Изменение устройств с {current_devices} до {new_devices_count}"
),
payment_method=PaymentMethod.BALANCE,
)
subscription.device_limit = new_devices_count
subscription.updated_at = datetime.utcnow()
await db.commit()
subscription_service = SubscriptionService()
try:
await subscription_service.update_remnawave_user(db, subscription)
except Exception as error:
logger.warning(
"Failed to update RemnaWave user after devices update: %s",
error,
)
await db.refresh(user)
await db.refresh(subscription)
message = (
f"Количество устройств обновлено: {current_devices}{new_devices_count}"
)
return MiniAppSubscriptionSettingsActionResponse(
success=True,
message=message,
balance_kopeks=user.balance_kopeks,
charged_kopeks=charged_amount,
)
@router.post("/subscription", response_model=MiniAppSubscriptionResponse)
async def get_subscription_details(
payload: MiniAppSubscriptionRequest,

View File

@@ -15,6 +15,33 @@ class MiniAppSubscriptionRequest(BaseModel):
init_data: str = Field(..., alias="initData")
class MiniAppSubscriptionSettingsRequest(BaseModel):
init_data: str = Field(..., alias="initData")
subscription_id: Optional[int] = Field(default=None, alias="subscriptionId")
class MiniAppSubscriptionServersUpdateRequest(BaseModel):
init_data: str = Field(..., alias="initData")
squads: List[str] = Field(default_factory=list)
servers: List[str] = Field(default_factory=list)
squad_uuids: List[str] = Field(default_factory=list)
server_uuids: List[str] = Field(default_factory=list)
subscription_id: Optional[int] = Field(default=None, alias="subscriptionId")
class MiniAppSubscriptionTrafficUpdateRequest(BaseModel):
init_data: str = Field(..., alias="initData")
traffic: Optional[int] = None
traffic_gb: Optional[int] = Field(default=None, alias="trafficGb")
subscription_id: Optional[int] = Field(default=None, alias="subscriptionId")
class MiniAppSubscriptionDevicesUpdateRequest(BaseModel):
init_data: str = Field(..., alias="initData")
devices: Optional[int] = None
subscription_id: Optional[int] = Field(default=None, alias="subscriptionId")
class MiniAppSubscriptionUser(BaseModel):
telegram_id: int
username: Optional[str] = None
@@ -88,6 +115,85 @@ class MiniAppDeviceRemovalResponse(BaseModel):
message: Optional[str] = None
class MiniAppSubscriptionSettingsServer(BaseModel):
uuid: str
name: str
price_kopeks: Optional[int] = None
price_label: Optional[str] = None
discount_percent: int = 0
is_connected: bool = False
is_available: bool = True
disabled_reason: Optional[str] = None
class MiniAppSubscriptionSettingsTrafficOption(BaseModel):
value: Optional[int] = None
label: str = ""
price_kopeks: Optional[int] = None
price_label: Optional[str] = None
is_current: bool = False
is_available: bool = True
description: Optional[str] = None
class MiniAppSubscriptionSettingsDeviceOption(BaseModel):
value: int
label: str
price_kopeks: Optional[int] = None
price_label: Optional[str] = None
class MiniAppSubscriptionSettingsCurrent(BaseModel):
servers: List[MiniAppConnectedServer] = Field(default_factory=list)
traffic_limit_gb: Optional[int] = None
traffic_limit_label: Optional[str] = None
device_limit: Optional[int] = None
class MiniAppSubscriptionSettingsServers(BaseModel):
available: List[MiniAppSubscriptionSettingsServer] = Field(default_factory=list)
min: int = 0
max: int = 0
can_update: bool = True
hint: Optional[str] = None
class MiniAppSubscriptionSettingsTraffic(BaseModel):
options: List[MiniAppSubscriptionSettingsTrafficOption] = Field(default_factory=list)
can_update: bool = True
current_value: Optional[int] = None
class MiniAppSubscriptionSettingsDevices(BaseModel):
options: List[MiniAppSubscriptionSettingsDeviceOption] = Field(default_factory=list)
can_update: bool = True
min: int = 0
max: int = 0
step: int = 1
current: Optional[int] = None
price_kopeks: Optional[int] = None
class MiniAppSubscriptionSettingsData(BaseModel):
subscription_id: Optional[int] = None
currency: str = "RUB"
current: MiniAppSubscriptionSettingsCurrent
servers: MiniAppSubscriptionSettingsServers
traffic: MiniAppSubscriptionSettingsTraffic
devices: MiniAppSubscriptionSettingsDevices
class MiniAppSubscriptionSettingsResponse(BaseModel):
settings: MiniAppSubscriptionSettingsData
class MiniAppSubscriptionSettingsActionResponse(BaseModel):
success: bool = True
message: Optional[str] = None
balance_kopeks: Optional[int] = None
charged_kopeks: int = 0
class MiniAppTransaction(BaseModel):
id: int
type: str