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