fix: prevent squad drop on admin subscription type change, require subscription for wheel spins
- Fix active_internal_squads sent unconditionally as [] clearing Remnawave squads - Fix dead code in _change_subscription_type (was_trial saved before mutation) - Block wheel spins for users without active subscription (API + bot handler) - Add has_subscription field to wheel config response - Refund Stars to balance if spin payment arrives without subscription - Fix SQL injection in promocode lookup (f-string → parameterized query) - Remove redundant get_or_create_wheel_config call in stars handler
This commit is contained in:
@@ -50,6 +50,12 @@ async def get_wheel_config(
|
||||
# Проверяем доступность
|
||||
availability = await wheel_service.check_availability(db, user)
|
||||
|
||||
# Проверяем наличие подписки
|
||||
from app.database.crud.subscription import get_subscription_by_user_id
|
||||
|
||||
subscription = await get_subscription_by_user_id(db, user.id)
|
||||
has_subscription = subscription is not None and subscription.is_active
|
||||
|
||||
prizes_display = [
|
||||
WheelPrizeDisplay(
|
||||
id=p.id,
|
||||
@@ -77,6 +83,7 @@ async def get_wheel_config(
|
||||
can_pay_days=availability.can_pay_days,
|
||||
user_balance_kopeks=availability.user_balance_kopeks,
|
||||
required_balance_kopeks=availability.required_balance_kopeks,
|
||||
has_subscription=has_subscription,
|
||||
)
|
||||
|
||||
|
||||
@@ -213,6 +220,16 @@ async def create_stars_invoice(
|
||||
detail='Оплата Stars не включена',
|
||||
)
|
||||
|
||||
# Проверяем наличие активной подписки
|
||||
from app.database.crud.subscription import get_subscription_by_user_id
|
||||
|
||||
subscription = await get_subscription_by_user_id(db, user.id)
|
||||
if not subscription or not subscription.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail='Для использования колеса необходима активная подписка',
|
||||
)
|
||||
|
||||
# Проверяем лимит спинов
|
||||
spins_today = await get_user_spins_today(db, user.id)
|
||||
if config.daily_spin_limit > 0 and spins_today >= config.daily_spin_limit:
|
||||
|
||||
@@ -60,6 +60,7 @@ class WheelConfigResponse(BaseModel):
|
||||
can_pay_days: bool = False
|
||||
user_balance_kopeks: int = 0
|
||||
required_balance_kopeks: int = 0
|
||||
has_subscription: bool = False
|
||||
|
||||
|
||||
class SpinAvailabilityResponse(BaseModel):
|
||||
|
||||
@@ -5110,10 +5110,11 @@ async def _change_subscription_type(db: AsyncSession, user_id: int, new_type: st
|
||||
old_type = 'триальной' if subscription.is_trial else 'платной'
|
||||
new_type_text = 'триальной' if new_is_trial else 'платной'
|
||||
|
||||
was_trial = subscription.is_trial
|
||||
subscription.is_trial = new_is_trial
|
||||
subscription.updated_at = datetime.now(UTC)
|
||||
|
||||
if not new_is_trial and subscription.is_trial:
|
||||
if not new_is_trial and was_trial:
|
||||
user = await get_user_by_id(db, user_id)
|
||||
if user:
|
||||
user.has_had_paid_subscription = True
|
||||
|
||||
@@ -37,8 +37,38 @@ async def _handle_wheel_spin_payment(
|
||||
)
|
||||
return False
|
||||
|
||||
# Проверяем наличие активной подписки
|
||||
from app.database.crud.subscription import get_subscription_by_user_id
|
||||
|
||||
subscription = await get_subscription_by_user_id(db, user.id)
|
||||
if not subscription or not subscription.is_active:
|
||||
# Конвертируем Stars в баланс как компенсацию
|
||||
rubles_fallback = TelegramStarsService.calculate_rubles_from_stars(stars_amount)
|
||||
kopeks_fallback = int((rubles_fallback * Decimal(100)).to_integral_value(rounding=ROUND_HALF_UP))
|
||||
from app.database.crud.user import add_user_balance
|
||||
from app.database.models import TransactionType
|
||||
|
||||
await add_user_balance(
|
||||
db,
|
||||
user,
|
||||
kopeks_fallback,
|
||||
f'Возврат за спин колеса без подписки ({stars_amount} Stars)',
|
||||
transaction_type=TransactionType.REFUND,
|
||||
)
|
||||
await db.commit()
|
||||
await message.answer(
|
||||
'❌ Для использования колеса удачи необходима активная подписка.\n'
|
||||
f'💰 {stars_amount} Stars возвращены на баланс в виде {kopeks_fallback / 100:.0f} ₽.',
|
||||
)
|
||||
logger.warning(
|
||||
'Wheel spin without subscription, refunded to balance',
|
||||
user_id=user.id,
|
||||
stars_amount=stars_amount,
|
||||
refund_kopeks=kopeks_fallback,
|
||||
)
|
||||
return False
|
||||
|
||||
# Выполняем спин напрямую (оплата уже прошла через Stars)
|
||||
prizes = await get_or_create_wheel_config(db)
|
||||
prizes = await get_wheel_prizes(db, config.id, active_only=True)
|
||||
|
||||
if not prizes:
|
||||
@@ -64,7 +94,11 @@ async def _handle_wheel_spin_payment(
|
||||
|
||||
promocode_id = None
|
||||
if generated_promocode:
|
||||
result = await db.execute(f"SELECT id FROM promocodes WHERE code = '{generated_promocode}'")
|
||||
from sqlalchemy import text
|
||||
|
||||
result = await db.execute(
|
||||
text('SELECT id FROM promocodes WHERE code = :code'), {'code': generated_promocode}
|
||||
)
|
||||
row = result.fetchone()
|
||||
if row:
|
||||
promocode_id = row[0]
|
||||
|
||||
@@ -253,9 +253,11 @@ class SubscriptionService:
|
||||
email=user.email,
|
||||
user_id=user.id,
|
||||
),
|
||||
active_internal_squads=subscription.connected_squads,
|
||||
)
|
||||
|
||||
if subscription.connected_squads:
|
||||
update_kwargs['active_internal_squads'] = subscription.connected_squads
|
||||
|
||||
if user_tag is not None:
|
||||
update_kwargs['tag'] = user_tag
|
||||
|
||||
@@ -298,9 +300,11 @@ class SubscriptionService:
|
||||
email=user.email,
|
||||
user_id=user.id,
|
||||
),
|
||||
active_internal_squads=subscription.connected_squads,
|
||||
)
|
||||
|
||||
if subscription.connected_squads:
|
||||
create_kwargs['active_internal_squads'] = subscription.connected_squads
|
||||
|
||||
if user_tag is not None:
|
||||
create_kwargs['tag'] = user_tag
|
||||
|
||||
@@ -392,9 +396,11 @@ class SubscriptionService:
|
||||
email=user.email,
|
||||
user_id=user.id,
|
||||
),
|
||||
active_internal_squads=subscription.connected_squads,
|
||||
)
|
||||
|
||||
if subscription.connected_squads:
|
||||
update_kwargs['active_internal_squads'] = subscription.connected_squads
|
||||
|
||||
if user_tag is not None:
|
||||
update_kwargs['tag'] = user_tag
|
||||
|
||||
|
||||
@@ -550,7 +550,11 @@ class FortuneWheelService:
|
||||
promocode_id = None
|
||||
if generated_promocode:
|
||||
# Получаем ID промокода
|
||||
result = await db.execute(f"SELECT id FROM promocodes WHERE code = '{generated_promocode}'")
|
||||
from sqlalchemy import text
|
||||
|
||||
result = await db.execute(
|
||||
text('SELECT id FROM promocodes WHERE code = :code'), {'code': generated_promocode}
|
||||
)
|
||||
row = result.fetchone()
|
||||
if row:
|
||||
promocode_id = row[0]
|
||||
|
||||
Reference in New Issue
Block a user