fix PR problems
This commit is contained in:
@@ -85,6 +85,7 @@ async def update_partner_settings(
|
||||
admin: User = Depends(require_permission('partners:settings')),
|
||||
):
|
||||
"""Update partner system settings."""
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
# Update in-memory settings
|
||||
@@ -104,8 +105,8 @@ async def update_partner_settings(
|
||||
# Persist to .env file
|
||||
try:
|
||||
env_file = Path('.env')
|
||||
if env_file.exists():
|
||||
lines = env_file.read_text().splitlines()
|
||||
if await asyncio.to_thread(env_file.exists):
|
||||
lines = (await asyncio.to_thread(env_file.read_text)).splitlines()
|
||||
updates: dict[str, str] = {}
|
||||
|
||||
if request.withdrawal_enabled is not None:
|
||||
@@ -143,7 +144,7 @@ async def update_partner_settings(
|
||||
if key not in updated_keys:
|
||||
new_lines.append(f'{key}={value}')
|
||||
|
||||
env_file.write_text('\n'.join(new_lines) + '\n')
|
||||
await asyncio.to_thread(env_file.write_text, '\n'.join(new_lines) + '\n')
|
||||
logger.info('Updated partner settings in .env file', admin_id=admin.id)
|
||||
except Exception as e:
|
||||
logger.warning('Failed to update .env file', error=e)
|
||||
|
||||
@@ -210,7 +210,7 @@ def _is_checkable(record: PendingPayment) -> bool:
|
||||
if record.method == PaymentMethod.YOOKASSA:
|
||||
return status_str in {'pending', 'waiting_for_capture'}
|
||||
if record.method == PaymentMethod.CRYPTOBOT:
|
||||
return status_str in {'active'}
|
||||
return status_str == 'active'
|
||||
if record.method == PaymentMethod.CLOUDPAYMENTS:
|
||||
return status_str in {'pending', 'authorized'}
|
||||
if record.method == PaymentMethod.FREEKASSA:
|
||||
|
||||
@@ -303,7 +303,7 @@ async def create_new_tariff(
|
||||
period_prices=period_prices_dict,
|
||||
allowed_squads=request.allowed_squads,
|
||||
server_traffic_limits=server_limits_dict,
|
||||
promo_group_ids=request.promo_group_ids if request.promo_group_ids else None,
|
||||
promo_group_ids=request.promo_group_ids or None,
|
||||
# Произвольное количество дней
|
||||
custom_days_enabled=request.custom_days_enabled,
|
||||
price_per_day_kopeks=request.price_per_day_kopeks,
|
||||
|
||||
@@ -246,6 +246,7 @@ async def update_ticket_settings(
|
||||
db: AsyncSession = Depends(get_cabinet_db),
|
||||
):
|
||||
"""Update ticket system settings."""
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
from app.services.support_settings_service import SupportSettingsService
|
||||
@@ -280,8 +281,8 @@ async def update_ticket_settings(
|
||||
# Try to persist to .env file
|
||||
try:
|
||||
env_file = Path('.env')
|
||||
if env_file.exists():
|
||||
lines = env_file.read_text().splitlines()
|
||||
if await asyncio.to_thread(env_file.exists):
|
||||
lines = (await asyncio.to_thread(env_file.read_text)).splitlines()
|
||||
updates = {}
|
||||
|
||||
if request.sla_enabled is not None:
|
||||
@@ -314,7 +315,7 @@ async def update_ticket_settings(
|
||||
if key not in updated_keys:
|
||||
new_lines.append(f'{key}={value}')
|
||||
|
||||
env_file.write_text('\n'.join(new_lines) + '\n')
|
||||
await asyncio.to_thread(env_file.write_text, '\n'.join(new_lines) + '\n')
|
||||
logger.info('Updated ticket settings in .env file')
|
||||
except Exception as e:
|
||||
logger.warning('Failed to update .env file', error=e)
|
||||
|
||||
@@ -774,7 +774,7 @@ async def register_email(
|
||||
},
|
||||
db=db,
|
||||
)
|
||||
custom_subject, custom_body = override if override else (None, None)
|
||||
custom_subject, custom_body = override or (None, None)
|
||||
|
||||
await asyncio.to_thread(
|
||||
email_service.send_verification_email,
|
||||
@@ -911,7 +911,7 @@ async def register_email_standalone(
|
||||
},
|
||||
db=db,
|
||||
)
|
||||
custom_subject, custom_body = override if override else (None, None)
|
||||
custom_subject, custom_body = override or (None, None)
|
||||
|
||||
await asyncio.to_thread(
|
||||
email_service.send_verification_email,
|
||||
@@ -1049,7 +1049,7 @@ async def resend_verification(
|
||||
},
|
||||
db=db,
|
||||
)
|
||||
custom_subject, custom_body = override if override else (None, None)
|
||||
custom_subject, custom_body = override or (None, None)
|
||||
|
||||
await asyncio.to_thread(
|
||||
email_service.send_verification_email,
|
||||
@@ -1356,7 +1356,7 @@ async def forgot_password(
|
||||
context={'username': user.first_name or '', 'reset_url': full_url, 'expire_hours': str(expire_hours)},
|
||||
db=db,
|
||||
)
|
||||
custom_subject, custom_body = override if override else (None, None)
|
||||
custom_subject, custom_body = override or (None, None)
|
||||
|
||||
await asyncio.to_thread(
|
||||
email_service.send_password_reset_email,
|
||||
@@ -1524,7 +1524,7 @@ async def request_email_change(
|
||||
},
|
||||
db=db,
|
||||
)
|
||||
custom_subject, custom_body = override if override else (None, None)
|
||||
custom_subject, custom_body = override or (None, None)
|
||||
|
||||
try:
|
||||
await asyncio.to_thread(
|
||||
@@ -1579,7 +1579,7 @@ async def request_email_change(
|
||||
},
|
||||
db=db,
|
||||
)
|
||||
custom_subject, custom_body = override if override else (None, None)
|
||||
custom_subject, custom_body = override or (None, None)
|
||||
|
||||
await asyncio.to_thread(
|
||||
email_service.send_email_change_code,
|
||||
|
||||
@@ -198,7 +198,7 @@ async def get_payment_methods(
|
||||
'description': description,
|
||||
}
|
||||
)
|
||||
options = formatted_options if formatted_options else None
|
||||
options = formatted_options or None
|
||||
|
||||
methods.append(
|
||||
PaymentMethodResponse(
|
||||
@@ -885,7 +885,7 @@ def _is_checkable(record: PendingPayment) -> bool:
|
||||
if record.method == PaymentMethod.YOOKASSA:
|
||||
return status in {'pending', 'waiting_for_capture'}
|
||||
if record.method == PaymentMethod.CRYPTOBOT:
|
||||
return status in {'active'}
|
||||
return status == 'active'
|
||||
if record.method == PaymentMethod.CLOUDPAYMENTS:
|
||||
return status in {'pending', 'authorized'}
|
||||
if record.method == PaymentMethod.FREEKASSA:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Branding routes for cabinet - logo, project name, and theme colors management."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -401,7 +402,7 @@ async def get_logo():
|
||||
"""
|
||||
logo_path = get_logo_path()
|
||||
|
||||
if logo_path is None or not logo_path.exists():
|
||||
if logo_path is None or not await asyncio.to_thread(logo_path.exists):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='No custom logo set')
|
||||
|
||||
# Determine media type from file extension
|
||||
@@ -470,7 +471,7 @@ async def upload_logo(
|
||||
)
|
||||
|
||||
# Ensure directory exists
|
||||
ensure_branding_dir()
|
||||
await asyncio.to_thread(ensure_branding_dir)
|
||||
|
||||
# Determine file extension from content type
|
||||
ext_map = {
|
||||
@@ -483,12 +484,12 @@ async def upload_logo(
|
||||
extension = ext_map.get(file.content_type, '.png')
|
||||
|
||||
# Remove old logo files with any extension
|
||||
for old_file in BRANDING_DIR.glob('logo.*'):
|
||||
old_file.unlink()
|
||||
for old_file in await asyncio.to_thread(lambda: list(BRANDING_DIR.glob('logo.*'))):
|
||||
await asyncio.to_thread(old_file.unlink)
|
||||
|
||||
# Save new logo
|
||||
logo_path = BRANDING_DIR / f'logo{extension}'
|
||||
logo_path.write_bytes(content)
|
||||
await asyncio.to_thread(logo_path.write_bytes, content)
|
||||
|
||||
# Mark that we have a custom logo
|
||||
await set_setting_value(db, BRANDING_LOGO_KEY, 'custom')
|
||||
@@ -517,8 +518,8 @@ async def delete_logo(
|
||||
):
|
||||
"""Delete custom logo and revert to letter. Admin only."""
|
||||
# Remove logo files
|
||||
for old_file in BRANDING_DIR.glob('logo.*'):
|
||||
old_file.unlink()
|
||||
for old_file in await asyncio.to_thread(lambda: list(BRANDING_DIR.glob('logo.*'))):
|
||||
await asyncio.to_thread(old_file.unlink)
|
||||
|
||||
# Update setting
|
||||
await set_setting_value(db, BRANDING_LOGO_KEY, 'default')
|
||||
|
||||
+23
-19
@@ -260,13 +260,17 @@ async def create_gift_purchase(
|
||||
detail='payment_method is required for gateway mode',
|
||||
)
|
||||
|
||||
purchase_kwargs: dict = {
|
||||
'gift_recipient_type': body.recipient_type,
|
||||
'gift_recipient_value': body.recipient_value,
|
||||
'gift_message': body.gift_message,
|
||||
} if has_recipient else {
|
||||
'gift_message': body.gift_message,
|
||||
}
|
||||
purchase_kwargs: dict = (
|
||||
{
|
||||
'gift_recipient_type': body.recipient_type,
|
||||
'gift_recipient_value': body.recipient_value,
|
||||
'gift_message': body.gift_message,
|
||||
}
|
||||
if has_recipient
|
||||
else {
|
||||
'gift_message': body.gift_message,
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
purchase = await create_purchase(
|
||||
@@ -345,13 +349,17 @@ async def create_gift_purchase(
|
||||
)
|
||||
|
||||
# Create purchase record
|
||||
balance_purchase_kwargs: dict = {
|
||||
'gift_recipient_type': body.recipient_type,
|
||||
'gift_recipient_value': body.recipient_value,
|
||||
'gift_message': body.gift_message,
|
||||
} if has_recipient else {
|
||||
'gift_message': body.gift_message,
|
||||
}
|
||||
balance_purchase_kwargs: dict = (
|
||||
{
|
||||
'gift_recipient_type': body.recipient_type,
|
||||
'gift_recipient_value': body.recipient_value,
|
||||
'gift_message': body.gift_message,
|
||||
}
|
||||
if has_recipient
|
||||
else {
|
||||
'gift_message': body.gift_message,
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
purchase = await create_purchase(
|
||||
@@ -497,11 +505,7 @@ async def get_gift_purchase_status(
|
||||
else:
|
||||
token_filter = GuestPurchase.token.startswith(token)
|
||||
|
||||
result = await db.execute(
|
||||
select(GuestPurchase)
|
||||
.options(selectinload(GuestPurchase.tariff))
|
||||
.where(token_filter)
|
||||
)
|
||||
result = await db.execute(select(GuestPurchase).options(selectinload(GuestPurchase.tariff)).where(token_filter))
|
||||
purchase = result.scalars().first()
|
||||
if purchase is None:
|
||||
raise HTTPException(
|
||||
|
||||
@@ -160,7 +160,7 @@ async def get_rules(
|
||||
db: AsyncSession = Depends(get_cabinet_db),
|
||||
):
|
||||
"""Get service rules - uses same function as bot."""
|
||||
requested_lang = language.split('-')[0].lower()
|
||||
requested_lang = language.split('-', maxsplit=1)[0].lower()
|
||||
|
||||
# Use the same function as bot to ensure consistent content
|
||||
content = await get_current_rules_content(db, requested_lang)
|
||||
|
||||
@@ -1469,7 +1469,7 @@ async def activate_trial(
|
||||
duration_days=trial_duration,
|
||||
traffic_limit_gb=trial_traffic_limit,
|
||||
device_limit=trial_device_limit,
|
||||
connected_squads=trial_squads if trial_squads else None,
|
||||
connected_squads=trial_squads or None,
|
||||
tariff_id=tariff_id_for_trial,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"""Schemas for Admin Users management in cabinet."""
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class UserStatusEnum(str, Enum):
|
||||
class UserStatusEnum(StrEnum):
|
||||
"""User status enum."""
|
||||
|
||||
ACTIVE = 'active'
|
||||
@@ -15,7 +15,7 @@ class UserStatusEnum(str, Enum):
|
||||
DELETED = 'deleted'
|
||||
|
||||
|
||||
class SubscriptionStatusEnum(str, Enum):
|
||||
class SubscriptionStatusEnum(StrEnum):
|
||||
"""Subscription status enum."""
|
||||
|
||||
TRIAL = 'trial'
|
||||
@@ -25,7 +25,7 @@ class SubscriptionStatusEnum(str, Enum):
|
||||
PENDING = 'pending'
|
||||
|
||||
|
||||
class SortByEnum(str, Enum):
|
||||
class SortByEnum(StrEnum):
|
||||
"""Sort options for users list."""
|
||||
|
||||
CREATED_AT = 'created_at'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Схемы для колеса удачи (Fortune Wheel)."""
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -9,14 +9,14 @@ from pydantic import BaseModel, Field
|
||||
# ==================== ENUMS ====================
|
||||
|
||||
|
||||
class WheelPaymentType(str, Enum):
|
||||
class WheelPaymentType(StrEnum):
|
||||
"""Способы оплаты спина."""
|
||||
|
||||
TELEGRAM_STARS = 'telegram_stars'
|
||||
SUBSCRIPTION_DAYS = 'subscription_days'
|
||||
|
||||
|
||||
class WheelPrizeType(str, Enum):
|
||||
class WheelPrizeType(StrEnum):
|
||||
"""Типы призов."""
|
||||
|
||||
SUBSCRIPTION_DAYS = 'subscription_days'
|
||||
|
||||
+22
-22
@@ -949,12 +949,12 @@ class Settings(BaseSettings):
|
||||
def get_test_email(self) -> str | None:
|
||||
"""Get test email for development/testing."""
|
||||
email = (self.TEST_EMAIL or '').strip().lower()
|
||||
return email if email else None
|
||||
return email or None
|
||||
|
||||
def get_test_email_password(self) -> str | None:
|
||||
"""Get test email password."""
|
||||
password = (self.TEST_EMAIL_PASSWORD or '').strip()
|
||||
return password if password else None
|
||||
return password or None
|
||||
|
||||
def is_test_email(self, email: str) -> bool:
|
||||
"""Check if email is the configured test email."""
|
||||
@@ -1519,7 +1519,7 @@ class Settings(BaseSettings):
|
||||
except (ValueError, IndexError):
|
||||
continue
|
||||
|
||||
return packages if packages else self.get_traffic_packages()
|
||||
return packages or self.get_traffic_packages()
|
||||
|
||||
def get_traffic_topup_price(self, gb: int | None) -> int:
|
||||
"""Возвращает цену докупки для указанного количества ГБ."""
|
||||
@@ -1609,7 +1609,7 @@ class Settings(BaseSettings):
|
||||
|
||||
def get_yookassa_display_name(self) -> str:
|
||||
name = (self.YOOKASSA_DISPLAY_NAME or '').strip()
|
||||
return name if name else 'YooKassa'
|
||||
return name or 'YooKassa'
|
||||
|
||||
def is_nalogo_enabled(self) -> bool:
|
||||
return self.NALOGO_ENABLED and self.NALOGO_INN is not None and self.NALOGO_PASSWORD is not None
|
||||
@@ -1629,14 +1629,14 @@ class Settings(BaseSettings):
|
||||
|
||||
def get_cryptobot_display_name(self) -> str:
|
||||
name = (self.CRYPTOBOT_DISPLAY_NAME or '').strip()
|
||||
return name if name else 'CryptoBot'
|
||||
return name or 'CryptoBot'
|
||||
|
||||
def is_heleket_enabled(self) -> bool:
|
||||
return self.HELEKET_ENABLED and self.HELEKET_MERCHANT_ID is not None and self.HELEKET_API_KEY is not None
|
||||
|
||||
def get_heleket_display_name(self) -> str:
|
||||
name = (self.HELEKET_DISPLAY_NAME or '').strip()
|
||||
return name if name else 'Heleket Crypto'
|
||||
return name or 'Heleket Crypto'
|
||||
|
||||
def is_mulenpay_enabled(self) -> bool:
|
||||
return (
|
||||
@@ -1674,7 +1674,7 @@ class Settings(BaseSettings):
|
||||
|
||||
def get_pal24_display_name(self) -> str:
|
||||
name = (self.PAL24_DISPLAY_NAME or '').strip()
|
||||
return name if name else 'PAL24'
|
||||
return name or 'PAL24'
|
||||
|
||||
def is_platega_enabled(self) -> bool:
|
||||
return self.PLATEGA_ENABLED and self.PLATEGA_MERCHANT_ID is not None and self.PLATEGA_SECRET is not None
|
||||
@@ -1754,7 +1754,7 @@ class Settings(BaseSettings):
|
||||
|
||||
def get_wata_display_name(self) -> str:
|
||||
name = (self.WATA_DISPLAY_NAME or '').strip()
|
||||
return name if name else 'Wata'
|
||||
return name or 'Wata'
|
||||
|
||||
def is_cloudpayments_enabled(self) -> bool:
|
||||
return (
|
||||
@@ -1765,7 +1765,7 @@ class Settings(BaseSettings):
|
||||
|
||||
def get_cloudpayments_display_name(self) -> str:
|
||||
name = (self.CLOUDPAYMENTS_DISPLAY_NAME or '').strip()
|
||||
return name if name else 'CloudPayments'
|
||||
return name or 'CloudPayments'
|
||||
|
||||
def is_freekassa_enabled(self) -> bool:
|
||||
return (
|
||||
@@ -1778,7 +1778,7 @@ class Settings(BaseSettings):
|
||||
|
||||
def get_freekassa_display_name(self) -> str:
|
||||
name = (self.FREEKASSA_DISPLAY_NAME or '').strip()
|
||||
return name if name else 'Freekassa'
|
||||
return name or 'Freekassa'
|
||||
|
||||
def get_freekassa_display_name_html(self) -> str:
|
||||
return html.escape(self.get_freekassa_display_name())
|
||||
@@ -1788,7 +1788,7 @@ class Settings(BaseSettings):
|
||||
|
||||
def get_freekassa_sbp_display_name(self) -> str:
|
||||
name = (self.FREEKASSA_SBP_DISPLAY_NAME or '').strip()
|
||||
return name if name else 'СБП (QR код)'
|
||||
return name or 'СБП (QR код)'
|
||||
|
||||
def get_freekassa_sbp_display_name_html(self) -> str:
|
||||
return html.escape(self.get_freekassa_sbp_display_name())
|
||||
@@ -1798,7 +1798,7 @@ class Settings(BaseSettings):
|
||||
|
||||
def get_freekassa_card_display_name(self) -> str:
|
||||
name = (self.FREEKASSA_CARD_DISPLAY_NAME or '').strip()
|
||||
return name if name else 'Карта РФ'
|
||||
return name or 'Карта РФ'
|
||||
|
||||
def get_freekassa_card_display_name_html(self) -> str:
|
||||
return html.escape(self.get_freekassa_card_display_name())
|
||||
@@ -1813,7 +1813,7 @@ class Settings(BaseSettings):
|
||||
|
||||
def get_kassa_ai_display_name(self) -> str:
|
||||
name = (self.KASSA_AI_DISPLAY_NAME or '').strip()
|
||||
return name if name else 'KassaAI'
|
||||
return name or 'KassaAI'
|
||||
|
||||
def get_kassa_ai_display_name_html(self) -> str:
|
||||
return html.escape(self.get_kassa_ai_display_name())
|
||||
@@ -1823,7 +1823,7 @@ class Settings(BaseSettings):
|
||||
|
||||
def get_riopay_display_name(self) -> str:
|
||||
name = (self.RIOPAY_DISPLAY_NAME or '').strip()
|
||||
return name if name else 'RioPay'
|
||||
return name or 'RioPay'
|
||||
|
||||
def get_riopay_display_name_html(self) -> str:
|
||||
return html.escape(self.get_riopay_display_name())
|
||||
@@ -1923,7 +1923,7 @@ class Settings(BaseSettings):
|
||||
'windows': ((self.HAPP_DOWNLOAD_LINK_WINDOWS or '').strip() or (self.HAPP_DOWNLOAD_LINK_PC or '').strip()),
|
||||
}
|
||||
link = links.get(platform_key)
|
||||
return link if link else None
|
||||
return link or None
|
||||
|
||||
def is_maintenance_mode(self) -> bool:
|
||||
return self.MAINTENANCE_MODE
|
||||
@@ -2011,7 +2011,7 @@ class Settings(BaseSettings):
|
||||
# т.к. в режиме classic цена складывается из серверов/трафика/устройств)
|
||||
periods = sorted(allowed_periods)
|
||||
|
||||
return periods if periods else [30, 90, 180]
|
||||
return periods or [30, 90, 180]
|
||||
|
||||
def get_available_renewal_periods(self) -> list[int]:
|
||||
"""
|
||||
@@ -2036,7 +2036,7 @@ class Settings(BaseSettings):
|
||||
# Возвращаем только разрешённые периоды (без фильтрации по цене)
|
||||
periods = sorted(allowed_periods)
|
||||
|
||||
return periods if periods else [30, 90, 180]
|
||||
return periods or [30, 90, 180]
|
||||
|
||||
def get_configured_subscription_periods(self) -> list[int]:
|
||||
"""
|
||||
@@ -2101,7 +2101,7 @@ class Settings(BaseSettings):
|
||||
|
||||
def get_telegram_stars_display_name(self) -> str:
|
||||
name = (self.TELEGRAM_STARS_DISPLAY_NAME or '').strip()
|
||||
return name if name else 'Telegram Stars'
|
||||
return name or 'Telegram Stars'
|
||||
|
||||
def stars_to_rubles(self, stars: int) -> float:
|
||||
return stars * self.get_stars_rate()
|
||||
@@ -2138,7 +2138,7 @@ class Settings(BaseSettings):
|
||||
|
||||
def get_backup_archive_password(self) -> str | None:
|
||||
password = (self.BACKUP_ARCHIVE_PASSWORD or '').strip()
|
||||
return password if password else None
|
||||
return password or None
|
||||
|
||||
# === Log Rotation Methods ===
|
||||
|
||||
@@ -2219,7 +2219,7 @@ class Settings(BaseSettings):
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
return packages if packages else self._get_fallback_traffic_packages()
|
||||
return packages or self._get_fallback_traffic_packages()
|
||||
|
||||
except Exception as e:
|
||||
logger.warning('ERROR PARSING CONFIG', error=e)
|
||||
@@ -2352,13 +2352,13 @@ class Settings(BaseSettings):
|
||||
|
||||
if contact.startswith(('t.me/', 'telegram.me/', 'telegram.dog/')):
|
||||
url = self.get_support_contact_url()
|
||||
return url if url else contact
|
||||
return url or contact
|
||||
|
||||
contact_without_prefix = contact.lstrip('@')
|
||||
|
||||
if '.' in contact_without_prefix:
|
||||
url = self.get_support_contact_url()
|
||||
return url if url else contact
|
||||
return url or contact
|
||||
|
||||
if re.fullmatch(r'[A-Za-z0-9_]{3,}', contact_without_prefix):
|
||||
return f'@{contact_without_prefix}'
|
||||
|
||||
@@ -187,7 +187,7 @@ async def get_primary_user_promo_group(db: AsyncSession, user_id: int) -> PromoG
|
||||
return None
|
||||
|
||||
# Первая в списке имеет максимальный приоритет (список уже отсортирован)
|
||||
return user_promo_groups[0].promo_group if user_promo_groups[0].promo_group else None
|
||||
return user_promo_groups[0].promo_group or None
|
||||
|
||||
except Exception as error:
|
||||
logger.error('Ошибка получения primary промогруппы пользователя', user_id=user_id, error=error)
|
||||
|
||||
@@ -8,7 +8,7 @@ def _aware(dt: datetime | None) -> datetime | None:
|
||||
return dt
|
||||
|
||||
|
||||
from enum import Enum
|
||||
from enum import Enum, StrEnum
|
||||
|
||||
from sqlalchemy import (
|
||||
JSON,
|
||||
@@ -3124,7 +3124,7 @@ class LandingPage(Base):
|
||||
return f"<LandingPage slug='{self.slug}' active={self.is_active}>"
|
||||
|
||||
|
||||
class GuestPurchaseStatus(str, Enum):
|
||||
class GuestPurchaseStatus(StrEnum):
|
||||
PENDING = 'pending'
|
||||
PAID = 'paid'
|
||||
DELIVERED = 'delivered'
|
||||
|
||||
@@ -29,7 +29,7 @@ async def show_blacklist_settings(callback: types.CallbackQuery, db_user: User,
|
||||
blacklist_count = len(await blacklist_service.get_all_blacklisted_users())
|
||||
|
||||
status_text = '✅ Включена' if is_enabled else '❌ Отключена'
|
||||
url_text = github_url if github_url else 'Не задан'
|
||||
url_text = github_url or 'Не задан'
|
||||
|
||||
text = f"""
|
||||
🔐 <b>Настройки черного списка</b>
|
||||
|
||||
@@ -1350,12 +1350,12 @@ async def _do_reconcile_logs(callback: CallbackQuery):
|
||||
await callback.answer('🔄 Анализирую логи платежей...', show_alert=False)
|
||||
|
||||
# Путь к файлу логов платежей (logs/current/)
|
||||
log_file_path = Path(settings.LOG_FILE).resolve()
|
||||
log_file_path = await asyncio.to_thread(Path(settings.LOG_FILE).resolve)
|
||||
log_dir = log_file_path.parent
|
||||
current_dir = log_dir / 'current'
|
||||
payments_log = current_dir / settings.LOG_PAYMENTS_FILE
|
||||
|
||||
if not payments_log.exists():
|
||||
if not await asyncio.to_thread(payments_log.exists):
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
'❌ <b>Файл логов не найден</b>\n\n'
|
||||
@@ -1491,12 +1491,12 @@ async def receipts_reconcile_logs_details_callback(callback: CallbackQuery):
|
||||
await callback.answer('🔄 Загружаю детали...', show_alert=False)
|
||||
|
||||
# Путь к логам (logs/current/)
|
||||
log_file_path = Path(settings.LOG_FILE).resolve()
|
||||
log_file_path = await asyncio.to_thread(Path(settings.LOG_FILE).resolve)
|
||||
log_dir = log_file_path.parent
|
||||
current_dir = log_dir / 'current'
|
||||
payments_log = current_dir / settings.LOG_PAYMENTS_FILE
|
||||
|
||||
if not payments_log.exists():
|
||||
if not await asyncio.to_thread(payments_log.exists):
|
||||
await callback.answer('❌ Файл логов не найден', show_alert=True)
|
||||
return
|
||||
|
||||
|
||||
@@ -188,7 +188,7 @@ def _is_checkable(record: PendingPayment) -> bool:
|
||||
if record.method == PaymentMethod.YOOKASSA:
|
||||
return status in {'pending', 'waiting_for_capture'}
|
||||
if record.method == PaymentMethod.CRYPTOBOT:
|
||||
return status in {'active'}
|
||||
return status == 'active'
|
||||
if record.method == PaymentMethod.FREEKASSA:
|
||||
return status in {'pending', 'created', ''}
|
||||
if record.method == PaymentMethod.KASSA_AI:
|
||||
@@ -380,7 +380,7 @@ def _build_payment_details_text(record: PendingPayment, *, texts, language: str)
|
||||
amount = f'{crypto_amount} {crypto_asset}'
|
||||
created = format_datetime(record.created_at)
|
||||
age = format_time_ago(record.created_at, language)
|
||||
raw_identifier = record.identifier if record.identifier else record.local_id
|
||||
raw_identifier = record.identifier or record.local_id
|
||||
identifier = html.escape(str(raw_identifier)) if raw_identifier is not None else '—'
|
||||
lines = [
|
||||
texts.t('ADMIN_PAYMENT_DETAILS_TITLE', '💳 <b>Payment details</b>'),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
@@ -756,8 +757,8 @@ async def _show_diagnostics_for_period(callback: types.CallbackQuery, db: AsyncS
|
||||
|
||||
# Информация о логах
|
||||
log_path = referral_diagnostics_service.log_path
|
||||
log_exists = log_path.exists()
|
||||
log_size = log_path.stat().st_size if log_exists else 0
|
||||
log_exists = await asyncio.to_thread(log_path.exists)
|
||||
log_size = (await asyncio.to_thread(log_path.stat)).st_size if log_exists else 0
|
||||
|
||||
text += f'\n<i>📂 {log_path.name}'
|
||||
if log_exists:
|
||||
@@ -1434,9 +1435,9 @@ async def receive_log_file(message: types.Message, db_user: User, db: AsyncSessi
|
||||
|
||||
finally:
|
||||
# Удаляем временный файл
|
||||
if temp_file_path and Path(temp_file_path).exists():
|
||||
if temp_file_path and await asyncio.to_thread(Path(temp_file_path).exists):
|
||||
try:
|
||||
Path(temp_file_path).unlink()
|
||||
await asyncio.to_thread(Path(temp_file_path).unlink)
|
||||
logger.info('🗑️ Временный файл удалён', temp_file_path=temp_file_path)
|
||||
except Exception as e:
|
||||
logger.error('Ошибка удаления временного файла', error=e)
|
||||
|
||||
@@ -81,7 +81,7 @@ def _split_text_into_pages(header: str, message_blocks: list[str], max_len: int
|
||||
if current.strip():
|
||||
pages.append(current)
|
||||
|
||||
return pages if pages else [header]
|
||||
return pages or [header]
|
||||
|
||||
|
||||
async def show_admin_tickets(callback: types.CallbackQuery, db_user: User, db: AsyncSession):
|
||||
|
||||
@@ -21,8 +21,8 @@ logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
FREEKASSA_SUB_METHODS = {
|
||||
'freekassa_sbp': {'payment_system_id': 44, 'get_name': lambda: settings.get_freekassa_sbp_display_name()},
|
||||
'freekassa_card': {'payment_system_id': 36, 'get_name': lambda: settings.get_freekassa_card_display_name()},
|
||||
'freekassa_sbp': {'payment_system_id': 44, 'get_name': settings.get_freekassa_sbp_display_name},
|
||||
'freekassa_card': {'payment_system_id': 36, 'get_name': settings.get_freekassa_card_display_name},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ def _split_into_pages(
|
||||
|
||||
pages.append((current_online, current_offline))
|
||||
|
||||
return pages if pages else [([], [])]
|
||||
return pages or [([], [])]
|
||||
|
||||
|
||||
def _format_server_lines(
|
||||
|
||||
@@ -970,7 +970,7 @@ async def handle_simple_subscription_payment_method(
|
||||
from aiogram.types import BufferedInputFile
|
||||
|
||||
# Используем qr_confirmation_data если доступно, иначе confirmation_url
|
||||
qr_data = qr_confirmation_data if qr_confirmation_data else confirmation_url
|
||||
qr_data = qr_confirmation_data or confirmation_url
|
||||
|
||||
# Создаем QR-код из полученных данных
|
||||
qr = qrcode.QRCode(version=1, box_size=10, border=5)
|
||||
|
||||
@@ -565,7 +565,7 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession,
|
||||
if referral_code:
|
||||
await state.update_data(referral_code=referral_code)
|
||||
|
||||
user = db_user if db_user else await get_user_by_telegram_id(db, message.from_user.id)
|
||||
user = db_user or await get_user_by_telegram_id(db, message.from_user.id)
|
||||
|
||||
if campaign and not campaign_notification_sent:
|
||||
try:
|
||||
|
||||
@@ -277,11 +277,7 @@ async def confirm_change_devices(callback: types.CallbackQuery, db_user: User, d
|
||||
|
||||
# Используем max_device_limit из тарифа если есть, иначе глобальную настройку
|
||||
tariff_max_devices = getattr(tariff, 'max_device_limit', None) if tariff else None
|
||||
effective_max = (
|
||||
tariff_max_devices
|
||||
if tariff_max_devices
|
||||
else (settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else None)
|
||||
)
|
||||
effective_max = tariff_max_devices or (settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else None)
|
||||
if effective_max and new_devices_count > effective_max:
|
||||
await callback.answer(
|
||||
texts.t(
|
||||
@@ -571,11 +567,7 @@ async def execute_change_devices(callback: types.CallbackQuery, db_user: User, d
|
||||
# Re-validate: prevent double-charge and max-limit violation
|
||||
if new_devices_count > current_devices:
|
||||
tariff_max_recheck = getattr(tariff, 'max_device_limit', None) if tariff else None
|
||||
max_devices = (
|
||||
tariff_max_recheck
|
||||
if tariff_max_recheck
|
||||
else (settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else None)
|
||||
)
|
||||
max_devices = tariff_max_recheck or (settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else None)
|
||||
if max_devices and new_devices_count > max_devices:
|
||||
if price > 0:
|
||||
user_refund = await db.execute(
|
||||
@@ -1139,11 +1131,7 @@ async def confirm_add_devices(callback: types.CallbackQuery, db_user: User, db:
|
||||
|
||||
# Используем max_device_limit из тарифа если есть, иначе глобальную настройку
|
||||
tariff_max_devices = getattr(tariff, 'max_device_limit', None) if tariff else None
|
||||
effective_max = (
|
||||
tariff_max_devices
|
||||
if tariff_max_devices
|
||||
else (settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else None)
|
||||
)
|
||||
effective_max = tariff_max_devices or (settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else None)
|
||||
if effective_max and new_total_devices > effective_max:
|
||||
await callback.answer(
|
||||
texts.t(
|
||||
@@ -1278,11 +1266,7 @@ async def confirm_add_devices(callback: types.CallbackQuery, db_user: User, db:
|
||||
actual_current = subscription.device_limit or 1
|
||||
actual_new = actual_current + devices_count
|
||||
tariff_max_recheck = getattr(tariff, 'max_device_limit', None) if tariff else None
|
||||
max_devices = (
|
||||
tariff_max_recheck
|
||||
if tariff_max_recheck
|
||||
else (settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else None)
|
||||
)
|
||||
max_devices = tariff_max_recheck or (settings.MAX_DEVICES_LIMIT if settings.MAX_DEVICES_LIMIT > 0 else None)
|
||||
if max_devices and actual_new > max_devices:
|
||||
# Concurrent purchase exceeded limit — refund
|
||||
user_refund = await db.execute(
|
||||
|
||||
@@ -313,7 +313,7 @@ async def show_subscription_info(callback: types.CallbackQuery, db_user: User, d
|
||||
devices_used_str = str(devices_used)
|
||||
|
||||
servers_names = await get_servers_display_names(subscription.connected_squads)
|
||||
servers_display = servers_names if servers_names else texts.t('SUBSCRIPTION_NO_SERVERS', 'Нет серверов')
|
||||
servers_display = servers_names or texts.t('SUBSCRIPTION_NO_SERVERS', 'Нет серверов')
|
||||
|
||||
# Получаем информацию о тарифе для режима тарифов
|
||||
tariff_info_block = ''
|
||||
|
||||
@@ -504,7 +504,7 @@ def _split_text_into_pages(header: str, message_blocks: list[str], max_len: int
|
||||
if current.strip():
|
||||
pages.append(current)
|
||||
|
||||
return pages if pages else [header]
|
||||
return pages or [header]
|
||||
|
||||
|
||||
async def view_ticket(callback: types.CallbackQuery, db_user: User, db: AsyncSession):
|
||||
|
||||
@@ -398,7 +398,7 @@ class ChannelCheckerMiddleware(BaseMiddleware):
|
||||
# Notify user about deactivation
|
||||
try:
|
||||
normalized = _normalize_channels(channels)
|
||||
texts = get_texts(user.language if user.language else DEFAULT_LANGUAGE)
|
||||
texts = get_texts(user.language or DEFAULT_LANGUAGE)
|
||||
notification_text = texts.t(
|
||||
'SUBSCRIPTION_DEACTIVATED_CHANNEL_UNSUBSCRIBE',
|
||||
'🚫 Ваша подписка приостановлена, так как вы отписались от канала.\n\n'
|
||||
@@ -468,7 +468,7 @@ class ChannelCheckerMiddleware(BaseMiddleware):
|
||||
|
||||
# Notify user about reactivation
|
||||
try:
|
||||
texts = get_texts(user.language if user.language else DEFAULT_LANGUAGE)
|
||||
texts = get_texts(user.language or DEFAULT_LANGUAGE)
|
||||
notification_text = texts.t(
|
||||
'SUBSCRIPTION_REACTIVATED_CHANNEL_SUBSCRIBE',
|
||||
'✅ Ваша подписка восстановлена!\n\nСпасибо, что подписались на канал. VPN снова работает.',
|
||||
|
||||
@@ -347,7 +347,7 @@ class BackupService:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
staging_dir = temp_path / 'backup'
|
||||
staging_dir.mkdir(parents=True, exist_ok=True)
|
||||
await asyncio.to_thread(staging_dir.mkdir, True, True)
|
||||
|
||||
database_info = await self._dump_database(staging_dir, include_logs=include_logs)
|
||||
database_info.setdefault('tables_count', overview.get('tables_count', 0))
|
||||
@@ -376,10 +376,10 @@ class BackupService:
|
||||
|
||||
mode = 'w:gz' if compress else 'w'
|
||||
with tarfile.open(backup_path, mode) as tar:
|
||||
for item in staging_dir.iterdir():
|
||||
for item in await asyncio.to_thread(lambda: list(staging_dir.iterdir())):
|
||||
tar.add(item, arcname=item.name)
|
||||
|
||||
file_size = backup_path.stat().st_size
|
||||
file_size = (await asyncio.to_thread(backup_path.stat)).st_size
|
||||
|
||||
await self._cleanup_old_backups()
|
||||
|
||||
@@ -415,7 +415,7 @@ class BackupService:
|
||||
logger.info('📄 Начинаем восстановление из', backup_file_path=backup_file_path)
|
||||
|
||||
backup_path = Path(backup_file_path)
|
||||
if not backup_path.exists():
|
||||
if not await asyncio.to_thread(backup_path.exists):
|
||||
return False, f'❌ Файл бекапа не найден: {backup_file_path}'
|
||||
|
||||
if self._is_archive_backup(backup_path):
|
||||
@@ -473,7 +473,11 @@ class BackupService:
|
||||
if pg_dump_path:
|
||||
dump_path = staging_dir / 'database.sql'
|
||||
await self._dump_postgres(dump_path, pg_dump_path)
|
||||
size = dump_path.stat().st_size if dump_path.exists() else 0
|
||||
size = (
|
||||
(await asyncio.to_thread(dump_path.stat)).st_size
|
||||
if await asyncio.to_thread(dump_path.exists)
|
||||
else 0
|
||||
)
|
||||
return {
|
||||
'type': 'postgresql',
|
||||
'path': dump_path.name,
|
||||
@@ -488,7 +492,7 @@ class BackupService:
|
||||
|
||||
dump_path = staging_dir / 'database.sqlite'
|
||||
await self._dump_sqlite(dump_path)
|
||||
size = dump_path.stat().st_size if dump_path.exists() else 0
|
||||
size = (await asyncio.to_thread(dump_path.stat)).st_size if await asyncio.to_thread(dump_path.exists) else 0
|
||||
return {
|
||||
'type': 'sqlite',
|
||||
'path': dump_path.name,
|
||||
@@ -516,9 +520,9 @@ class BackupService:
|
||||
]
|
||||
|
||||
logger.info('📦 Экспорт PostgreSQL через pg_dump ...', pg_dump_path=pg_dump_path)
|
||||
dump_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
await asyncio.to_thread(dump_path.parent.mkdir, True, True)
|
||||
|
||||
with dump_path.open('wb') as dump_file:
|
||||
with open(dump_path, 'wb') as dump_file:
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*command,
|
||||
stdout=dump_file,
|
||||
@@ -558,7 +562,7 @@ class BackupService:
|
||||
async with aiofiles.open(dump_path, 'w', encoding='utf-8') as dump_file:
|
||||
await dump_file.write(json_lib.dumps(dump_structure, ensure_ascii=False, indent=2))
|
||||
|
||||
size = dump_path.stat().st_size if dump_path.exists() else 0
|
||||
size = (await asyncio.to_thread(dump_path.stat)).st_size if await asyncio.to_thread(dump_path.exists) else 0
|
||||
|
||||
logger.info('✅ PostgreSQL экспортирован через ORM в JSON', dump_path=dump_path)
|
||||
|
||||
@@ -575,10 +579,10 @@ class BackupService:
|
||||
|
||||
async def _dump_sqlite(self, dump_path: Path):
|
||||
sqlite_path = Path(settings.SQLITE_PATH)
|
||||
if not sqlite_path.exists():
|
||||
if not await asyncio.to_thread(sqlite_path.exists):
|
||||
raise FileNotFoundError(f'SQLite база данных не найдена по пути {sqlite_path}')
|
||||
|
||||
dump_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
await asyncio.to_thread(dump_path.parent.mkdir, True, True)
|
||||
await asyncio.to_thread(shutil.copy2, sqlite_path, dump_path)
|
||||
logger.info('✅ SQLite база данных скопирована', dump_path=dump_path)
|
||||
|
||||
@@ -662,11 +666,11 @@ class BackupService:
|
||||
async def _collect_files(self, staging_dir: Path, include_logs: bool) -> list[dict[str, Any]]:
|
||||
files_info: list[dict[str, Any]] = []
|
||||
files_dir = staging_dir / 'files'
|
||||
files_dir.mkdir(parents=True, exist_ok=True)
|
||||
await asyncio.to_thread(files_dir.mkdir, True, True)
|
||||
|
||||
if include_logs and settings.LOG_FILE:
|
||||
log_path = Path(settings.LOG_FILE)
|
||||
if log_path.exists():
|
||||
if await asyncio.to_thread(log_path.exists):
|
||||
dest = files_dir / log_path.name
|
||||
await asyncio.to_thread(shutil.copy2, log_path, dest)
|
||||
files_info.append(
|
||||
@@ -676,8 +680,8 @@ class BackupService:
|
||||
}
|
||||
)
|
||||
|
||||
if not files_info and files_dir.exists():
|
||||
files_dir.rmdir()
|
||||
if not files_info and await asyncio.to_thread(files_dir.exists):
|
||||
await asyncio.to_thread(files_dir.rmdir)
|
||||
|
||||
return files_info
|
||||
|
||||
@@ -688,7 +692,7 @@ class BackupService:
|
||||
'items': 0,
|
||||
}
|
||||
|
||||
if not self.data_dir.exists():
|
||||
if not await asyncio.to_thread(self.data_dir.exists):
|
||||
return snapshot_info
|
||||
|
||||
counter = {'items': 0}
|
||||
@@ -732,7 +736,7 @@ class BackupService:
|
||||
tar.extractall(temp_path, filter='data')
|
||||
|
||||
metadata_path = temp_path / 'metadata.json'
|
||||
if not metadata_path.exists():
|
||||
if not await asyncio.to_thread(metadata_path.exists):
|
||||
return False, '❌ Метаданные бекапа отсутствуют'
|
||||
|
||||
async with aiofiles.open(metadata_path, encoding='utf-8') as meta_file:
|
||||
@@ -758,7 +762,7 @@ class BackupService:
|
||||
await self._restore_sqlite(dump_file, clear_existing)
|
||||
|
||||
data_dir = temp_path / 'data'
|
||||
if data_dir.exists():
|
||||
if await asyncio.to_thread(data_dir.exists):
|
||||
await self._restore_data_snapshot(data_dir, clear_existing)
|
||||
|
||||
if files_info:
|
||||
@@ -775,7 +779,7 @@ class BackupService:
|
||||
return True, message
|
||||
|
||||
async def _restore_postgres(self, dump_path: Path, clear_existing: bool):
|
||||
if not dump_path.exists():
|
||||
if not await asyncio.to_thread(dump_path.exists):
|
||||
raise FileNotFoundError(f'Dump PostgreSQL не найден: {dump_path}')
|
||||
|
||||
psql_path = self._resolve_command_path('psql', 'PSQL_PATH')
|
||||
@@ -833,7 +837,7 @@ class BackupService:
|
||||
logger.info('✅ PostgreSQL восстановлен', dump_path=dump_path)
|
||||
|
||||
async def _restore_postgres_json(self, dump_path: Path, clear_existing: bool):
|
||||
if not dump_path.exists():
|
||||
if not await asyncio.to_thread(dump_path.exists):
|
||||
raise FileNotFoundError(f'JSON дамп PostgreSQL не найден: {dump_path}')
|
||||
|
||||
async with aiofiles.open(dump_path, encoding='utf-8') as dump_file:
|
||||
@@ -853,20 +857,20 @@ class BackupService:
|
||||
logger.info('✅ PostgreSQL восстановлен из ORM JSON', dump_path=dump_path)
|
||||
|
||||
async def _restore_sqlite(self, dump_path: Path, clear_existing: bool):
|
||||
if not dump_path.exists():
|
||||
if not await asyncio.to_thread(dump_path.exists):
|
||||
raise FileNotFoundError(f'SQLite файл не найден: {dump_path}')
|
||||
|
||||
target_path = Path(settings.SQLITE_PATH)
|
||||
target_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
await asyncio.to_thread(target_path.parent.mkdir, True, True)
|
||||
|
||||
if clear_existing and target_path.exists():
|
||||
target_path.unlink()
|
||||
if clear_existing and await asyncio.to_thread(target_path.exists):
|
||||
await asyncio.to_thread(target_path.unlink)
|
||||
|
||||
await asyncio.to_thread(shutil.copy2, dump_path, target_path)
|
||||
logger.info('✅ SQLite база восстановлена', target_path=target_path)
|
||||
|
||||
async def _restore_data_snapshot(self, source_dir: Path, clear_existing: bool):
|
||||
if not source_dir.exists():
|
||||
if not await asyncio.to_thread(source_dir.exists):
|
||||
return
|
||||
|
||||
def _restore():
|
||||
@@ -891,7 +895,7 @@ class BackupService:
|
||||
logger.info('📁 Снимок директории data восстановлен')
|
||||
|
||||
async def _restore_files(self, files_info: list[dict[str, Any]], temp_path: Path):
|
||||
allowed_base = self.data_dir.resolve()
|
||||
allowed_base = await asyncio.to_thread(self.data_dir.resolve)
|
||||
|
||||
for file_info in files_info:
|
||||
relative_path = file_info.get('relative_path')
|
||||
@@ -899,21 +903,22 @@ class BackupService:
|
||||
if not relative_path or not target_path:
|
||||
continue
|
||||
|
||||
target_resolved = target_path.resolve()
|
||||
target_resolved = await asyncio.to_thread(target_path.resolve)
|
||||
if not str(target_resolved).startswith(str(allowed_base) + os.sep) and target_resolved != allowed_base:
|
||||
logger.warning('Заблокирована запись за пределами data_dir', target_path=target_path)
|
||||
continue
|
||||
|
||||
source_file = (temp_path / relative_path).resolve()
|
||||
if not str(source_file).startswith(str(temp_path.resolve()) + os.sep):
|
||||
source_file = await asyncio.to_thread((temp_path / relative_path).resolve)
|
||||
temp_path_resolved = await asyncio.to_thread(temp_path.resolve)
|
||||
if not str(source_file).startswith(str(temp_path_resolved) + os.sep):
|
||||
logger.warning('Path traversal в relative_path', relative_path=relative_path)
|
||||
continue
|
||||
|
||||
if not source_file.exists():
|
||||
if not await asyncio.to_thread(source_file.exists):
|
||||
logger.warning('Файл отсутствует в архиве', relative_path=relative_path)
|
||||
continue
|
||||
|
||||
target_resolved.parent.mkdir(parents=True, exist_ok=True)
|
||||
await asyncio.to_thread(target_resolved.parent.mkdir, True, True)
|
||||
await asyncio.to_thread(shutil.copy2, source_file, target_resolved)
|
||||
logger.info('📁 Файл восстановлен', target_resolved=target_resolved)
|
||||
|
||||
@@ -1573,8 +1578,10 @@ class BackupService:
|
||||
backups = []
|
||||
|
||||
try:
|
||||
for backup_file in sorted(self.backup_dir.glob('backup_*'), reverse=True):
|
||||
if not backup_file.is_file():
|
||||
for backup_file in sorted(
|
||||
await asyncio.to_thread(lambda: list(self.backup_dir.glob('backup_*'))), reverse=True
|
||||
):
|
||||
if not await asyncio.to_thread(backup_file.is_file):
|
||||
continue
|
||||
|
||||
try:
|
||||
@@ -1598,7 +1605,7 @@ class BackupService:
|
||||
backup_structure = json_lib.load(f)
|
||||
metadata = backup_structure.get('metadata', {})
|
||||
|
||||
file_stats = backup_file.stat()
|
||||
file_stats = await asyncio.to_thread(backup_file.stat)
|
||||
|
||||
backup_info = {
|
||||
'filename': backup_file.name,
|
||||
@@ -1626,7 +1633,7 @@ class BackupService:
|
||||
|
||||
except Exception as e:
|
||||
logger.error('Ошибка чтения метаданных', backup_file=backup_file, error=e)
|
||||
file_stats = backup_file.stat()
|
||||
file_stats = await asyncio.to_thread(backup_file.stat)
|
||||
backups.append(
|
||||
{
|
||||
'filename': backup_file.name,
|
||||
@@ -1651,14 +1658,15 @@ class BackupService:
|
||||
|
||||
async def delete_backup(self, backup_filename: str) -> tuple[bool, str]:
|
||||
try:
|
||||
backup_path = (self.backup_dir / backup_filename).resolve()
|
||||
if not str(backup_path).startswith(str(self.backup_dir.resolve()) + os.sep):
|
||||
backup_path = await asyncio.to_thread((self.backup_dir / backup_filename).resolve)
|
||||
backup_dir_resolved = await asyncio.to_thread(self.backup_dir.resolve)
|
||||
if not str(backup_path).startswith(str(backup_dir_resolved) + os.sep):
|
||||
return False, '❌ Недопустимое имя файла бекапа'
|
||||
|
||||
if not backup_path.is_file():
|
||||
if not await asyncio.to_thread(backup_path.is_file):
|
||||
return False, f'❌ Файл бекапа не найден: {backup_filename}'
|
||||
|
||||
backup_path.unlink()
|
||||
await asyncio.to_thread(backup_path.unlink)
|
||||
message = f'✅ Бекап {backup_filename} удален'
|
||||
logger.info(message)
|
||||
|
||||
@@ -1826,9 +1834,9 @@ class BackupService:
|
||||
await self.bot.send_document(**send_kwargs)
|
||||
logger.info('Бекап отправлен в чат', chat_id=chat_id)
|
||||
|
||||
if temp_zip_path and Path(temp_zip_path).exists():
|
||||
if temp_zip_path and await asyncio.to_thread(Path(temp_zip_path).exists):
|
||||
try:
|
||||
Path(temp_zip_path).unlink()
|
||||
await asyncio.to_thread(Path(temp_zip_path).unlink)
|
||||
except Exception as cleanup_error:
|
||||
logger.warning('Не удалось удалить временный архив', cleanup_error=cleanup_error)
|
||||
|
||||
@@ -1838,7 +1846,7 @@ class BackupService:
|
||||
async def _create_password_protected_archive(self, file_path: str, password: str) -> str | None:
|
||||
try:
|
||||
source_path = Path(file_path)
|
||||
if not source_path.exists():
|
||||
if not await asyncio.to_thread(source_path.exists):
|
||||
logger.error('Исходный файл бекапа не найден', file_path=file_path)
|
||||
return None
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Enum classes for contest system."""
|
||||
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class GameType(str, Enum):
|
||||
class GameType(StrEnum):
|
||||
"""Types of daily contest games."""
|
||||
|
||||
QUEST_BUTTONS = 'quest_buttons'
|
||||
@@ -30,14 +30,14 @@ class GameType(str, Enum):
|
||||
}
|
||||
|
||||
|
||||
class RoundStatus(str, Enum):
|
||||
class RoundStatus(StrEnum):
|
||||
"""Contest round status."""
|
||||
|
||||
ACTIVE = 'active'
|
||||
FINISHED = 'finished'
|
||||
|
||||
|
||||
class PrizeType(str, Enum):
|
||||
class PrizeType(StrEnum):
|
||||
"""Types of prizes for contests."""
|
||||
|
||||
DAYS = 'days'
|
||||
|
||||
@@ -78,8 +78,8 @@ class LogRotationService:
|
||||
|
||||
async def initialize(self) -> None:
|
||||
"""Создать необходимые директории."""
|
||||
self.current_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.archive_dir.mkdir(parents=True, exist_ok=True)
|
||||
await asyncio.to_thread(self.current_dir.mkdir, True, True)
|
||||
await asyncio.to_thread(self.archive_dir.mkdir, True, True)
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Запустить сервис ротации."""
|
||||
@@ -170,7 +170,7 @@ class LogRotationService:
|
||||
# Собираем файлы для архивации
|
||||
files_to_archive: list[tuple[Path, str]] = []
|
||||
for name, log_path in self.log_files.items():
|
||||
if log_path.exists() and log_path.stat().st_size > 0:
|
||||
if await asyncio.to_thread(log_path.exists) and (await asyncio.to_thread(log_path.stat)).st_size > 0:
|
||||
files_to_archive.append((log_path, f'{name}.log'))
|
||||
|
||||
if not files_to_archive:
|
||||
@@ -184,7 +184,7 @@ class LogRotationService:
|
||||
if archive_path:
|
||||
# Очищаем текущие лог-файлы
|
||||
for log_path, _ in files_to_archive:
|
||||
log_path.write_text('')
|
||||
await asyncio.to_thread(log_path.write_text, '')
|
||||
|
||||
# Очистка старых архивов
|
||||
await self._cleanup_old_archives()
|
||||
@@ -247,12 +247,12 @@ class LogRotationService:
|
||||
keep_days = settings.LOG_ROTATION_KEEP_DAYS
|
||||
cutoff_date = datetime.now(get_local_timezone()) - timedelta(days=keep_days)
|
||||
|
||||
if not self.archive_dir.exists():
|
||||
if not await asyncio.to_thread(self.archive_dir.exists):
|
||||
return
|
||||
|
||||
# Ищем файлы вида logs_YYYY-MM-DD.tar.gz или logs_YYYY-MM-DD.tar
|
||||
for archive_file in self.archive_dir.iterdir():
|
||||
if not archive_file.is_file():
|
||||
for archive_file in await asyncio.to_thread(lambda: list(self.archive_dir.iterdir())):
|
||||
if not await asyncio.to_thread(archive_file.is_file):
|
||||
continue
|
||||
|
||||
# Извлекаем дату из имени файла logs_YYYY-MM-DD.tar.gz
|
||||
@@ -267,7 +267,7 @@ class LogRotationService:
|
||||
file_date = file_date.replace(tzinfo=get_local_timezone())
|
||||
|
||||
if file_date < cutoff_date:
|
||||
archive_file.unlink()
|
||||
await asyncio.to_thread(archive_file.unlink)
|
||||
logger.info('Удален старый архив логов', archive_file_name=archive_file.name)
|
||||
except ValueError:
|
||||
# Пропускаем файлы с некорректным форматом имени
|
||||
@@ -287,7 +287,7 @@ class LogRotationService:
|
||||
topic_id = settings.get_log_rotation_topic_id()
|
||||
|
||||
try:
|
||||
file_size_kb = archive_path.stat().st_size / 1024
|
||||
file_size_kb = (await asyncio.to_thread(archive_path.stat)).st_size / 1024
|
||||
caption = (
|
||||
f'<b>Логи бота</b>\n'
|
||||
f'Дата: {date_str}\n'
|
||||
|
||||
@@ -111,7 +111,11 @@ class MonitoringService:
|
||||
logger.debug('Пропуск уведомления: пользователь недоступен', user_id=user.id, status=user.status)
|
||||
return None
|
||||
|
||||
if settings.ENABLE_LOGO_MODE and LOGO_PATH.exists() and not caption_exceeds_telegram_limit(text):
|
||||
if (
|
||||
settings.ENABLE_LOGO_MODE
|
||||
and await asyncio.to_thread(LOGO_PATH.exists)
|
||||
and not caption_exceeds_telegram_limit(text)
|
||||
):
|
||||
try:
|
||||
from app.utils.message_patch import _cache_logo_file_id, get_logo_media
|
||||
|
||||
|
||||
@@ -767,7 +767,7 @@ class YooKassaPaymentMixin:
|
||||
)
|
||||
|
||||
# Используем full_user для форматирования реферальной информации, чтобы избежать проблем с ленивой загрузкой
|
||||
user_for_referrer = full_user if full_user else user
|
||||
user_for_referrer = full_user or user
|
||||
referrer_info = format_referrer_info(user_for_referrer)
|
||||
topup_status = '🆕 Первое пополнение' if was_first_topup else '🔄 Пополнение'
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@ def _get_platega_sub_options() -> list[dict] | None:
|
||||
'name': info.get('title') or info.get('name') or f'Platega {method_code}',
|
||||
}
|
||||
)
|
||||
return options if options else None
|
||||
return options or None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
@@ -527,7 +527,7 @@ class MiniAppSubscriptionPurchaseService:
|
||||
value = int(package.get('gb') or 0)
|
||||
price_per_month = int(package.get('price') or 0)
|
||||
discounted_per_month, discount_value = _apply_percentage_discount(price_per_month, discount_percent)
|
||||
label = texts.format_traffic(value if value else 0)
|
||||
label = texts.format_traffic(value or 0)
|
||||
options.append(
|
||||
PurchaseTrafficOption(
|
||||
value=value,
|
||||
@@ -587,7 +587,7 @@ class MiniAppSubscriptionPurchaseService:
|
||||
options=options,
|
||||
min_selectable=1 if options else 0,
|
||||
max_selectable=len(options),
|
||||
default_selection=default_selection if default_selection else [opt.uuid for opt in options[:1]],
|
||||
default_selection=default_selection or [opt.uuid for opt in options[:1]],
|
||||
hint=None,
|
||||
)
|
||||
|
||||
|
||||
@@ -1536,7 +1536,7 @@ class SubscriptionService:
|
||||
logger.warning('Не удалось предзагрузить тариф подписки', subscription_id=sub.id, error=exc)
|
||||
|
||||
# Вычисляем стратегию сброса трафика один раз — все подписки одного тарифа
|
||||
sample_tariff = subscriptions[0].tariff if subscriptions[0].tariff else None
|
||||
sample_tariff = subscriptions[0].tariff or None
|
||||
traffic_strategy = get_traffic_reset_strategy(sample_tariff)
|
||||
|
||||
# Параллельная синхронизация: один API-клиент, только HTTP-вызовы внутри gather
|
||||
|
||||
@@ -429,7 +429,7 @@ async def get_service_rules(
|
||||
language: str = Query('ru', min_length=2, max_length=10),
|
||||
fallback: bool = Query(True),
|
||||
) -> ServiceRulesResponse:
|
||||
requested_lang = language.split('-')[0].lower()
|
||||
requested_lang = language.split('-', maxsplit=1)[0].lower()
|
||||
rules = await get_rules_by_language(db, requested_lang)
|
||||
|
||||
if not rules and fallback:
|
||||
@@ -467,7 +467,7 @@ async def clear_service_rules(
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
language: str = Query('ru', min_length=2, max_length=10),
|
||||
) -> Response:
|
||||
lang = language.split('-')[0].lower()
|
||||
lang = language.split('-', maxsplit=1)[0].lower()
|
||||
await clear_all_rules(db, lang)
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -479,7 +479,7 @@ async def get_service_rules_history(
|
||||
language: str = Query('ru', min_length=2, max_length=10),
|
||||
limit: int = Query(10, ge=1, le=100),
|
||||
) -> ServiceRulesHistoryResponse:
|
||||
lang = language.split('-')[0].lower()
|
||||
lang = language.split('-', maxsplit=1)[0].lower()
|
||||
history = await get_all_rules_versions(db, lang, limit=limit)
|
||||
items = [_serialize_rules(item) for item in history]
|
||||
return ServiceRulesHistoryResponse(
|
||||
@@ -500,7 +500,7 @@ async def restore_service_rules_version(
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
language: str = Query('ru', min_length=2, max_length=10),
|
||||
) -> ServiceRulesResponse:
|
||||
lang = language.split('-')[0].lower()
|
||||
lang = language.split('-', maxsplit=1)[0].lower()
|
||||
restored = await restore_rules_version(db, rule_id, language=lang)
|
||||
if not restored:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, 'Rules version not found')
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class ButtonType(str, Enum):
|
||||
class ButtonType(StrEnum):
|
||||
"""Тип кнопки меню."""
|
||||
|
||||
BUILTIN = 'builtin' # Встроенная кнопка с callback_data
|
||||
@@ -18,7 +18,7 @@ class ButtonType(str, Enum):
|
||||
CALLBACK = 'callback' # Кастомная кнопка с любым callback_data
|
||||
|
||||
|
||||
class ButtonVisibility(str, Enum):
|
||||
class ButtonVisibility(StrEnum):
|
||||
"""Видимость кнопки."""
|
||||
|
||||
ALL = 'all' # Видна всем
|
||||
@@ -27,7 +27,7 @@ class ButtonVisibility(str, Enum):
|
||||
SUBSCRIBERS = 'subscribers' # Только подписчикам
|
||||
|
||||
|
||||
class ButtonOpenMode(str, Enum):
|
||||
class ButtonOpenMode(StrEnum):
|
||||
"""Режим открытия кнопки."""
|
||||
|
||||
CALLBACK = 'callback' # Отправляет callback_data боту (по умолчанию)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
@@ -386,7 +386,7 @@ class MiniAppPaymentMethodsRequest(BaseModel):
|
||||
init_data: str = Field(..., alias='initData')
|
||||
|
||||
|
||||
class MiniAppPaymentIntegrationType(str, Enum):
|
||||
class MiniAppPaymentIntegrationType(StrEnum):
|
||||
IFRAME = 'iframe'
|
||||
REDIRECT = 'redirect'
|
||||
|
||||
|
||||
@@ -723,7 +723,7 @@ async def main():
|
||||
|
||||
timeline.log_section(
|
||||
'Активные webhook endpoints',
|
||||
webhook_lines if webhook_lines else ['Нет активных endpoints'],
|
||||
webhook_lines or ['Нет активных endpoints'],
|
||||
icon='🎯',
|
||||
)
|
||||
|
||||
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
"""add riopay_payments table
|
||||
|
||||
Revision ID: 0032
|
||||
Revises: 0031
|
||||
Revision ID: 0036
|
||||
Revises: 0035
|
||||
Create Date: 2026-03-08
|
||||
|
||||
"""
|
||||
@@ -11,8 +11,8 @@ from typing import Sequence, Union
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = '0032'
|
||||
down_revision: Union[str, None] = '0031'
|
||||
revision: str = '0036'
|
||||
down_revision: Union[str, None] = '0035'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
@@ -166,6 +166,7 @@ ignore = [
|
||||
'TC001',
|
||||
'TC002',
|
||||
'TC003',
|
||||
'A003',
|
||||
'N805',
|
||||
'N815',
|
||||
'RET504',
|
||||
|
||||
@@ -246,7 +246,7 @@ class TestPayloadIntegration:
|
||||
|
||||
mock_state = AsyncMock()
|
||||
mock_state.get_data = AsyncMock(return_value=state_storage)
|
||||
mock_state.set_data = AsyncMock(side_effect=lambda d: state_storage.update(d))
|
||||
mock_state.set_data = AsyncMock(side_effect=state_storage.update)
|
||||
|
||||
mock_message = _create_mock_message('/start ref_flow_test', 111222)
|
||||
|
||||
|
||||
@@ -341,7 +341,7 @@ async def test_process_cryptobot_webhook_success(monkeypatch: pytest.MonkeyPatch
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
'app.services.admin_notification_service',
|
||||
SimpleNamespace(AdminNotificationService=lambda bot: DummyAdminService2(bot)),
|
||||
SimpleNamespace(AdminNotificationService=DummyAdminService2),
|
||||
)
|
||||
|
||||
class DummyAsyncSession:
|
||||
@@ -354,7 +354,7 @@ async def test_process_cryptobot_webhook_success(monkeypatch: pytest.MonkeyPatch
|
||||
async def rollback(self): # pragma: no cover - defensive stub
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(cryptobot_module, 'AsyncSessionLocal', lambda: DummyAsyncSession())
|
||||
monkeypatch.setattr(cryptobot_module, 'AsyncSessionLocal', DummyAsyncSession)
|
||||
monkeypatch.setattr(payment_service_module.currency_converter, 'usd_to_rub', AsyncMock(return_value=140.0))
|
||||
monkeypatch.setattr(type(settings), 'format_price', lambda self, amount: f'{amount / 100:.2f}₽', raising=False)
|
||||
service.build_topup_success_keyboard = AsyncMock(return_value=None)
|
||||
@@ -490,7 +490,7 @@ async def test_process_heleket_webhook_success(monkeypatch: pytest.MonkeyPatch)
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
'app.services.admin_notification_service',
|
||||
SimpleNamespace(AdminNotificationService=lambda bot: DummyAdminService(bot)),
|
||||
SimpleNamespace(AdminNotificationService=DummyAdminService),
|
||||
)
|
||||
|
||||
service.build_topup_success_keyboard = AsyncMock(return_value=None)
|
||||
@@ -597,7 +597,7 @@ async def test_process_yookassa_webhook_success(monkeypatch: pytest.MonkeyPatch)
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
'app.services.admin_notification_service',
|
||||
SimpleNamespace(AdminNotificationService=lambda bot: DummyAdminService(bot)),
|
||||
SimpleNamespace(AdminNotificationService=DummyAdminService),
|
||||
)
|
||||
service.build_topup_success_keyboard = AsyncMock(return_value=None)
|
||||
|
||||
@@ -694,7 +694,7 @@ async def test_process_yookassa_webhook_uses_remote_status(monkeypatch: pytest.M
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
'app.services.admin_notification_service',
|
||||
SimpleNamespace(AdminNotificationService=lambda bot: DummyAdminService(bot)),
|
||||
SimpleNamespace(AdminNotificationService=DummyAdminService),
|
||||
)
|
||||
|
||||
service.build_topup_success_keyboard = AsyncMock(return_value=None)
|
||||
@@ -895,7 +895,7 @@ async def test_process_yookassa_webhook_restores_missing_payment(
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
'app.services.admin_notification_service',
|
||||
SimpleNamespace(AdminNotificationService=lambda bot: DummyAdminService(bot)),
|
||||
SimpleNamespace(AdminNotificationService=DummyAdminService),
|
||||
)
|
||||
service.build_topup_success_keyboard = AsyncMock(return_value=None)
|
||||
|
||||
@@ -1050,7 +1050,7 @@ async def test_process_pal24_callback_success(monkeypatch: pytest.MonkeyPatch) -
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
'app.services.admin_notification_service',
|
||||
SimpleNamespace(AdminNotificationService=lambda bot: DummyAdminServicePal(bot)),
|
||||
SimpleNamespace(AdminNotificationService=DummyAdminServicePal),
|
||||
)
|
||||
|
||||
user_cart_stub = SimpleNamespace(user_cart_service=SimpleNamespace(has_user_cart=AsyncMock(return_value=True)))
|
||||
@@ -1215,7 +1215,7 @@ async def test_get_pal24_payment_status_auto_finalize(monkeypatch: pytest.Monkey
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
'app.services.admin_notification_service',
|
||||
SimpleNamespace(AdminNotificationService=lambda bot: DummyAdminService(bot)),
|
||||
SimpleNamespace(AdminNotificationService=DummyAdminService),
|
||||
)
|
||||
|
||||
user_cart_stub = SimpleNamespace(user_cart_service=SimpleNamespace(has_user_cart=AsyncMock(return_value=False)))
|
||||
|
||||
@@ -141,7 +141,7 @@ async def test_auto_purchase_saved_cart_after_topup_success(monkeypatch):
|
||||
|
||||
monkeypatch.setattr(
|
||||
'app.services.subscription_auto_purchase_service.MiniAppSubscriptionPurchaseService',
|
||||
lambda: DummyMiniAppService(),
|
||||
DummyMiniAppService,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
'app.services.subscription_auto_purchase_service.user_cart_service.get_user_cart',
|
||||
|
||||
@@ -107,7 +107,7 @@ async def test_initialize_skips_db_value_for_env_override(monkeypatch):
|
||||
|
||||
monkeypatch.setattr(
|
||||
'app.services.system_settings_service.AsyncSessionLocal',
|
||||
lambda: DummySession(),
|
||||
DummySession,
|
||||
)
|
||||
|
||||
async def fake_sync():
|
||||
|
||||
@@ -52,7 +52,7 @@ def test_init_without_credentials(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
async def test_create_payment_success(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
_prepare_config(monkeypatch)
|
||||
monkeypatch.setattr(settings, 'YOOKASSA_DEFAULT_RECEIPT_EMAIL', None, raising=False)
|
||||
monkeypatch.setattr(asyncio, 'get_running_loop', lambda: DummyLoop(), raising=False)
|
||||
monkeypatch.setattr(asyncio, 'get_running_loop', DummyLoop, raising=False)
|
||||
|
||||
captured_config: dict[str, tuple[str, str]] = {}
|
||||
|
||||
@@ -106,7 +106,7 @@ async def test_create_payment_without_contacts(monkeypatch: pytest.MonkeyPatch)
|
||||
_prepare_config(monkeypatch)
|
||||
monkeypatch.setattr(settings, 'YOOKASSA_DEFAULT_RECEIPT_EMAIL', None, raising=False)
|
||||
monkeypatch.setattr(Configuration, 'configure', lambda *args, **kwargs: None, raising=False)
|
||||
monkeypatch.setattr(asyncio, 'get_running_loop', lambda: DummyLoop(), raising=False)
|
||||
monkeypatch.setattr(asyncio, 'get_running_loop', DummyLoop, raising=False)
|
||||
monkeypatch.setattr(
|
||||
YooKassaPayment,
|
||||
'create',
|
||||
@@ -142,7 +142,7 @@ async def test_create_payment_returns_none_when_not_configured(monkeypatch: pyte
|
||||
@pytest.mark.anyio('asyncio')
|
||||
async def test_create_sbp_payment_success(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
_prepare_config(monkeypatch)
|
||||
monkeypatch.setattr(asyncio, 'get_running_loop', lambda: DummyLoop(), raising=False)
|
||||
monkeypatch.setattr(asyncio, 'get_running_loop', DummyLoop, raising=False)
|
||||
monkeypatch.setattr(Configuration, 'configure', lambda *args, **kwargs: None, raising=False)
|
||||
monkeypatch.setattr(settings, 'YOOKASSA_DEFAULT_RECEIPT_EMAIL', 'fallback@example.com', raising=False)
|
||||
|
||||
|
||||
@@ -1032,7 +1032,7 @@ async def test_create_payment_link_stars_normalizes_amount(monkeypatch):
|
||||
async def fake_resolve_user(db, init_data):
|
||||
return types.SimpleNamespace(id=7, language='ru'), {}
|
||||
|
||||
monkeypatch.setattr(miniapp, 'PaymentService', lambda bot: DummyPaymentService(bot))
|
||||
monkeypatch.setattr(miniapp, 'PaymentService', DummyPaymentService)
|
||||
monkeypatch.setattr(miniapp, 'Bot', DummyBot)
|
||||
monkeypatch.setattr(miniapp, '_resolve_user_from_init_data', fake_resolve_user)
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ def test_perform_sync_rebuilds_service_on_each_run(monkeypatch):
|
||||
|
||||
monkeypatch.setattr(
|
||||
'app.services.remnawave_sync_service.AsyncSessionLocal',
|
||||
lambda: DummySession(),
|
||||
DummySession,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
'app.services.remnawave_sync_service.sync_with_remnawave',
|
||||
|
||||
Reference in New Issue
Block a user