fix: complete datetime.utcnow() → datetime.now(UTC) migration

- Migrate 660+ datetime.utcnow() across 153 files to datetime.now(UTC)
- Migrate 30+ datetime.now() without UTC to datetime.now(UTC)
- Convert all 170 DateTime columns to DateTime(timezone=True)
- Add migrate_datetime_to_timestamptz() in universal_migration with SET LOCAL timezone='UTC' safety
- Remove 70+ .replace(tzinfo=None) workarounds
- Fix utcfromtimestamp → fromtimestamp(..., tz=UTC)
- Fix fromtimestamp() without tz= (system_logs, backup_service, referral_diagnostics)
- Fix fromisoformat/isoparse to ensure aware output (platega, yookassa, wata, miniapp, nalogo)
- Fix strptime() to add .replace(tzinfo=UTC) (backup_service, referral_diagnostics)
- Fix datetime.combine() to include tzinfo=UTC (remnawave_sync, traffic_monitoring)
- Fix datetime.max/datetime.min sentinels with .replace(tzinfo=UTC)
- Rename panel_datetime_to_naive_utc → panel_datetime_to_utc
- Remove DTZ003 from ruff ignore list
This commit is contained in:
Fringg
2026-02-17 04:45:40 +03:00
parent ff21b27b98
commit eb18994b7d
177 changed files with 1280 additions and 1261 deletions
+5 -5
View File
@@ -1,7 +1,7 @@
"""Email verification token generation and validation.""" """Email verification token generation and validation."""
import secrets import secrets
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
from app.config import settings from app.config import settings
@@ -24,7 +24,7 @@ def get_email_change_expires_at() -> datetime:
Datetime when the email change code expires Datetime when the email change code expires
""" """
minutes = settings.get_cabinet_email_change_code_expire_minutes() minutes = settings.get_cabinet_email_change_code_expire_minutes()
return datetime.utcnow() + timedelta(minutes=minutes) return datetime.now(UTC) + timedelta(minutes=minutes)
def generate_verification_token() -> str: def generate_verification_token() -> str:
@@ -55,7 +55,7 @@ def get_verification_expires_at() -> datetime:
Datetime when the verification token expires Datetime when the verification token expires
""" """
hours = settings.get_cabinet_email_verification_expire_hours() hours = settings.get_cabinet_email_verification_expire_hours()
return datetime.utcnow() + timedelta(hours=hours) return datetime.now(UTC) + timedelta(hours=hours)
def get_password_reset_expires_at() -> datetime: def get_password_reset_expires_at() -> datetime:
@@ -66,7 +66,7 @@ def get_password_reset_expires_at() -> datetime:
Datetime when the password reset token expires Datetime when the password reset token expires
""" """
hours = settings.get_cabinet_password_reset_expire_hours() hours = settings.get_cabinet_password_reset_expire_hours()
return datetime.utcnow() + timedelta(hours=hours) return datetime.now(UTC) + timedelta(hours=hours)
def is_token_expired(expires_at: datetime | None) -> bool: def is_token_expired(expires_at: datetime | None) -> bool:
@@ -81,4 +81,4 @@ def is_token_expired(expires_at: datetime | None) -> bool:
""" """
if expires_at is None: if expires_at is None:
return True return True
return datetime.utcnow() > expires_at return datetime.now(UTC) > expires_at
+6 -6
View File
@@ -1,6 +1,6 @@
"""JWT token handling for cabinet authentication.""" """JWT token handling for cabinet authentication."""
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
from typing import Any from typing import Any
import jwt import jwt
@@ -23,13 +23,13 @@ def create_access_token(user_id: int, telegram_id: int | None = None) -> str:
Encoded JWT access token Encoded JWT access token
""" """
expire_minutes = settings.get_cabinet_access_token_expire_minutes() expire_minutes = settings.get_cabinet_access_token_expire_minutes()
expires = datetime.utcnow() + timedelta(minutes=expire_minutes) expires = datetime.now(UTC) + timedelta(minutes=expire_minutes)
payload = { payload = {
'sub': str(user_id), 'sub': str(user_id),
'type': 'access', 'type': 'access',
'exp': expires, 'exp': expires,
'iat': datetime.utcnow(), 'iat': datetime.now(UTC),
} }
# Добавляем telegram_id только если он есть # Добавляем telegram_id только если он есть
@@ -51,13 +51,13 @@ def create_refresh_token(user_id: int) -> str:
Encoded JWT refresh token Encoded JWT refresh token
""" """
expire_days = settings.get_cabinet_refresh_token_expire_days() expire_days = settings.get_cabinet_refresh_token_expire_days()
expires = datetime.utcnow() + timedelta(days=expire_days) expires = datetime.now(UTC) + timedelta(days=expire_days)
payload = { payload = {
'sub': str(user_id), 'sub': str(user_id),
'type': 'refresh', 'type': 'refresh',
'exp': expires, 'exp': expires,
'iat': datetime.utcnow(), 'iat': datetime.now(UTC),
} }
secret = settings.get_cabinet_jwt_secret() secret = settings.get_cabinet_jwt_secret()
@@ -108,4 +108,4 @@ def get_token_payload(token: str, expected_type: str = 'access') -> dict[str, An
def get_refresh_token_expires_at() -> datetime: def get_refresh_token_expires_at() -> datetime:
"""Get the expiration datetime for a new refresh token.""" """Get the expiration datetime for a new refresh token."""
expire_days = settings.get_cabinet_refresh_token_expire_days() expire_days = settings.get_cabinet_refresh_token_expire_days()
return datetime.utcnow() + timedelta(days=expire_days) return datetime.now(UTC) + timedelta(days=expire_days)
+5 -5
View File
@@ -3,7 +3,7 @@
import hashlib import hashlib
import hmac import hmac
import json import json
from datetime import datetime from datetime import UTC, datetime
from typing import Any from typing import Any
from urllib.parse import parse_qsl, unquote from urllib.parse import parse_qsl, unquote
@@ -34,8 +34,8 @@ def validate_telegram_login_widget(data: dict[str, Any], max_age_seconds: int =
if auth_date: if auth_date:
try: try:
# Use UTC timestamp to avoid timezone issues # Use UTC timestamp to avoid timezone issues
auth_time = datetime.utcfromtimestamp(int(auth_date)) auth_time = datetime.fromtimestamp(int(auth_date), tz=UTC)
age = (datetime.utcnow() - auth_time).total_seconds() age = (datetime.now(UTC) - auth_time).total_seconds()
if age > max_age_seconds: if age > max_age_seconds:
return False return False
except (ValueError, TypeError, OSError): except (ValueError, TypeError, OSError):
@@ -81,8 +81,8 @@ def validate_telegram_init_data(init_data: str, max_age_seconds: int = 86400) ->
if auth_date: if auth_date:
try: try:
# Use UTC timestamp to avoid timezone issues # Use UTC timestamp to avoid timezone issues
auth_time = datetime.utcfromtimestamp(int(auth_date)) auth_time = datetime.fromtimestamp(int(auth_date), tz=UTC)
age = (datetime.utcnow() - auth_time).total_seconds() age = (datetime.now(UTC) - auth_time).total_seconds()
if age > max_age_seconds: if age > max_age_seconds:
return None return None
except (ValueError, TypeError, OSError): except (ValueError, TypeError, OSError):
+2 -2
View File
@@ -1,6 +1,6 @@
"""Admin routes for broadcasts in cabinet.""" """Admin routes for broadcasts in cabinet."""
from datetime import datetime from datetime import UTC, datetime
import structlog import structlog
from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi import APIRouter, Depends, HTTPException, Query, status
@@ -724,7 +724,7 @@ async def stop_broadcast(
broadcast.status = 'cancelling' broadcast.status = 'cancelling'
else: else:
broadcast.status = 'cancelled' broadcast.status = 'cancelled'
broadcast.completed_at = datetime.utcnow() broadcast.completed_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(broadcast) await db.refresh(broadcast)
+5 -5
View File
@@ -1,7 +1,7 @@
"""Admin routes for pinned messages in cabinet.""" """Admin routes for pinned messages in cabinet."""
import time import time
from datetime import datetime from datetime import UTC, datetime
import structlog import structlog
from aiogram import Bot from aiogram import Bot
@@ -227,7 +227,7 @@ async def update_pinned_message(
if payload.send_on_every_start is not None: if payload.send_on_every_start is not None:
msg.send_on_every_start = payload.send_on_every_start msg.send_on_every_start = payload.send_on_every_start
msg.updated_at = datetime.utcnow() msg.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(msg) await db.refresh(msg)
@@ -255,7 +255,7 @@ async def update_pinned_message_settings(
if payload.send_on_every_start is not None: if payload.send_on_every_start is not None:
msg.send_on_every_start = payload.send_on_every_start msg.send_on_every_start = payload.send_on_every_start
msg.updated_at = datetime.utcnow() msg.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(msg) await db.refresh(msg)
@@ -332,11 +332,11 @@ async def activate_pinned_message(
await db.execute( await db.execute(
update(PinnedMessage) update(PinnedMessage)
.where(PinnedMessage.is_active.is_(True)) .where(PinnedMessage.is_active.is_(True))
.values(is_active=False, updated_at=datetime.utcnow()) .values(is_active=False, updated_at=datetime.now(UTC))
) )
msg.is_active = True msg.is_active = True
msg.updated_at = datetime.utcnow() msg.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(msg) await db.refresh(msg)
+2 -2
View File
@@ -162,9 +162,9 @@ def _normalize_datetime(value: datetime | None) -> datetime | None:
if value is None: if value is None:
return None return None
if value.tzinfo is not None and value.utcoffset() is not None: if value.tzinfo is not None and value.utcoffset() is not None:
return value.astimezone(UTC).replace(tzinfo=None) return value.astimezone(UTC)
if value.tzinfo is not None: if value.tzinfo is not None:
return value.replace(tzinfo=None) return value
return value return value
+2 -2
View File
@@ -1,6 +1,6 @@
"""Admin routes for RemnaWave management in cabinet.""" """Admin routes for RemnaWave management in cabinet."""
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
from typing import Any from typing import Any
import structlog import structlog
@@ -338,7 +338,7 @@ async def get_node_usage(
service = _get_service() service = _get_service()
_ensure_configured(service) _ensure_configured(service)
end_dt = end or datetime.utcnow() end_dt = end or datetime.now(UTC)
start_dt = start or (end_dt - timedelta(days=7)) start_dt = start or (end_dt - timedelta(days=7))
if start_dt >= end_dt: if start_dt >= end_dt:
+5 -5
View File
@@ -2,7 +2,7 @@
import sys import sys
import time import time
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
import structlog import structlog
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
@@ -258,7 +258,7 @@ async def get_dashboard_stats(
sub_stats = await get_subscriptions_statistics(db) sub_stats = await get_subscriptions_statistics(db)
# Get financial statistics # Get financial statistics
now = datetime.utcnow() now = datetime.now(UTC)
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
trans_stats = await get_transactions_statistics(db, month_start, now) trans_stats = await get_transactions_statistics(db, month_start, now)
@@ -503,7 +503,7 @@ async def _get_tariff_stats(db: AsyncSession) -> TariffStats | None:
logger.info('📊 Нет тарифов в системе, пропускаем статистику') logger.info('📊 Нет тарифов в системе, пропускаем статистику')
return None return None
now = datetime.utcnow() now = datetime.now(UTC)
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
week_ago = now - timedelta(days=7) week_ago = now - timedelta(days=7)
month_ago = now - timedelta(days=30) month_ago = now - timedelta(days=30)
@@ -601,7 +601,7 @@ async def get_top_referrers(
): ):
"""Get top referrers with earnings breakdown by period.""" """Get top referrers with earnings breakdown by period."""
try: try:
now = datetime.utcnow() now = datetime.now(UTC)
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
week_ago = now - timedelta(days=7) week_ago = now - timedelta(days=7)
month_ago = now - timedelta(days=30) month_ago = now - timedelta(days=30)
@@ -874,7 +874,7 @@ async def get_recent_payments(
): ):
"""Get recent payments with user info.""" """Get recent payments with user info."""
try: try:
now = datetime.utcnow() now = datetime.now(UTC)
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
week_ago = now - timedelta(days=7) week_ago = now - timedelta(days=7)
+6 -6
View File
@@ -1,7 +1,7 @@
"""Admin tickets routes for cabinet.""" """Admin tickets routes for cabinet."""
import math import math
from datetime import datetime from datetime import UTC, datetime
import structlog import structlog
from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi import APIRouter, Depends, HTTPException, Query, status
@@ -447,13 +447,13 @@ async def reply_to_ticket(
user_id=ticket.user_id, user_id=ticket.user_id,
message_text=request.message, message_text=request.message,
is_from_admin=True, is_from_admin=True,
created_at=datetime.utcnow(), created_at=datetime.now(UTC),
) )
db.add(message) db.add(message)
# Update ticket status to answered # Update ticket status to answered
ticket.status = 'answered' ticket.status = 'answered'
ticket.updated_at = datetime.utcnow() ticket.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(message) await db.refresh(message)
@@ -522,9 +522,9 @@ async def update_ticket_status(
) )
ticket.status = request.status ticket.status = request.status
ticket.updated_at = datetime.utcnow() ticket.updated_at = datetime.now(UTC)
if request.status == 'closed': if request.status == 'closed':
ticket.closed_at = datetime.utcnow() ticket.closed_at = datetime.now(UTC)
else: else:
ticket.closed_at = None ticket.closed_at = None
@@ -581,7 +581,7 @@ async def update_ticket_priority(
) )
ticket.priority = request.priority ticket.priority = request.priority
ticket.updated_at = datetime.utcnow() ticket.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(ticket) await db.refresh(ticket)
+3 -3
View File
@@ -1,6 +1,6 @@
"""Admin routes for version and release information.""" """Admin routes for version and release information."""
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
import aiohttp import aiohttp
import structlog import structlog
@@ -53,7 +53,7 @@ async def _fetch_cabinet_releases(force: bool = False) -> list[dict]:
global _cabinet_last_check global _cabinet_last_check
if not force and _cabinet_cache.get('releases') and _cabinet_last_check: if not force and _cabinet_cache.get('releases') and _cabinet_last_check:
if datetime.now() - _cabinet_last_check < timedelta(seconds=_CACHE_TTL): if datetime.now(UTC) - _cabinet_last_check < timedelta(seconds=_CACHE_TTL):
return _cabinet_cache['releases'] return _cabinet_cache['releases']
url = f'https://api.github.com/repos/{CABINET_REPO}/releases' url = f'https://api.github.com/repos/{CABINET_REPO}/releases'
@@ -75,7 +75,7 @@ async def _fetch_cabinet_releases(force: bool = False) -> list[dict]:
} }
) )
_cabinet_cache['releases'] = releases _cabinet_cache['releases'] = releases
_cabinet_last_check = datetime.now() _cabinet_last_check = datetime.now(UTC)
logger.info('Fetched cabinet releases from GitHub', releases_count=len(releases)) logger.info('Fetched cabinet releases from GitHub', releases_count=len(releases))
return releases return releases
logger.warning('GitHub API returned status for cabinet releases', response_status=response.status) logger.warning('GitHub API returned status for cabinet releases', response_status=response.status)
+43 -52
View File
@@ -1,6 +1,6 @@
"""Admin routes for managing users in cabinet.""" """Admin routes for managing users in cabinet."""
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
import structlog import structlog
from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi import APIRouter, Depends, HTTPException, Query, status
@@ -35,7 +35,7 @@ from app.database.models import (
User, User,
UserStatus, UserStatus,
) )
from app.utils.timezone import panel_datetime_to_naive_utc from app.utils.timezone import panel_datetime_to_utc
from ..dependencies import get_cabinet_db, get_current_admin_user from ..dependencies import get_cabinet_db, get_current_admin_user
from ..schemas.users import ( from ..schemas.users import (
@@ -144,9 +144,9 @@ def _build_subscription_info(subscription: Subscription, tariff_name: str | None
is_active = False is_active = False
if subscription.end_date: if subscription.end_date:
delta = subscription.end_date - datetime.utcnow() delta = subscription.end_date - datetime.now(UTC)
days_remaining = max(0, delta.days) days_remaining = max(0, delta.days)
is_active = subscription.status == SubscriptionStatus.ACTIVE.value and subscription.end_date > datetime.utcnow() is_active = subscription.status == SubscriptionStatus.ACTIVE.value and subscription.end_date > datetime.now(UTC)
return UserSubscriptionInfo( return UserSubscriptionInfo(
id=subscription.id, id=subscription.id,
@@ -174,7 +174,7 @@ async def _build_subscription_info_async(db: AsyncSession, subscription: Subscri
tariff_name = tariff.name tariff_name = tariff.name
# Fetch traffic purchases # Fetch traffic purchases
now = datetime.utcnow() now = datetime.now(UTC)
tp_query = ( tp_query = (
select(TrafficPurchase) select(TrafficPurchase)
.where(TrafficPurchase.subscription_id == subscription.id) .where(TrafficPurchase.subscription_id == subscription.id)
@@ -225,13 +225,13 @@ async def _sync_subscription_to_panel(db: AsyncSession, user: User, subscription
is_active = ( is_active = (
subscription.status in (SubscriptionStatus.ACTIVE.value, SubscriptionStatus.TRIAL.value) subscription.status in (SubscriptionStatus.ACTIVE.value, SubscriptionStatus.TRIAL.value)
and subscription.end_date and subscription.end_date
and subscription.end_date > datetime.utcnow() and subscription.end_date > datetime.now(UTC)
) )
panel_status = PanelUserStatus.ACTIVE if is_active else PanelUserStatus.DISABLED panel_status = PanelUserStatus.ACTIVE if is_active else PanelUserStatus.DISABLED
expire_at = subscription.end_date expire_at = subscription.end_date
if expire_at and expire_at <= datetime.utcnow(): if expire_at and expire_at <= datetime.now(UTC):
expire_at = datetime.utcnow() + timedelta(minutes=1) expire_at = datetime.now(UTC) + timedelta(minutes=1)
username = settings.format_remnawave_username( username = settings.format_remnawave_username(
full_name=user.full_name, full_name=user.full_name,
@@ -310,7 +310,7 @@ async def _sync_subscription_to_panel(db: AsyncSession, user: User, subscription
# Create new user # Create new user
create_kwargs = { create_kwargs = {
'username': username, 'username': username,
'expire_at': expire_at or (datetime.utcnow() + timedelta(days=30)), 'expire_at': expire_at or (datetime.now(UTC) + timedelta(days=30)),
'status': panel_status, 'status': panel_status,
'traffic_limit_bytes': traffic_limit_bytes, 'traffic_limit_bytes': traffic_limit_bytes,
'traffic_limit_strategy': TrafficLimitStrategy.MONTH, 'traffic_limit_strategy': TrafficLimitStrategy.MONTH,
@@ -330,7 +330,7 @@ async def _sync_subscription_to_panel(db: AsyncSession, user: User, subscription
changes['panel_uuid'] = new_panel_user.uuid changes['panel_uuid'] = new_panel_user.uuid
logger.info('Created user in Remnawave panel', user_id=user.id, uuid=new_panel_user.uuid) logger.info('Created user in Remnawave panel', user_id=user.id, uuid=new_panel_user.uuid)
user.last_remnawave_sync = datetime.utcnow() user.last_remnawave_sync = datetime.now(UTC)
await db.commit() await db.commit()
return changes return changes
@@ -421,7 +421,7 @@ async def get_users_stats(
func.cast( func.cast(
and_( and_(
Subscription.status == SubscriptionStatus.ACTIVE.value, Subscription.status == SubscriptionStatus.ACTIVE.value,
Subscription.end_date > datetime.utcnow(), Subscription.end_date > datetime.now(UTC),
), ),
Integer, Integer,
) )
@@ -431,7 +431,7 @@ async def get_users_stats(
func.cast( func.cast(
or_( or_(
Subscription.status == SubscriptionStatus.EXPIRED.value, Subscription.status == SubscriptionStatus.EXPIRED.value,
Subscription.end_date <= datetime.utcnow(), Subscription.end_date <= datetime.now(UTC),
), ),
Integer, Integer,
) )
@@ -456,7 +456,7 @@ async def get_users_stats(
avg_balance = int(balance_row.avg or 0) if balance_row else 0 avg_balance = int(balance_row.avg or 0) if balance_row else 0
# Get activity stats # Get activity stats
now = datetime.utcnow() now = datetime.now(UTC)
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
week_ago = now - timedelta(days=7) week_ago = now - timedelta(days=7)
month_ago = now - timedelta(days=30) month_ago = now - timedelta(days=30)
@@ -756,7 +756,7 @@ async def get_user_node_usage(
if not service.is_configured: if not service.is_configured:
return UserNodeUsageResponse(items=[]) return UserNodeUsageResponse(items=[])
end_date = datetime.utcnow() end_date = datetime.now(UTC)
start_date = end_date - timedelta(days=30) start_date = end_date - timedelta(days=30)
start_str = start_date.strftime('%Y-%m-%d') start_str = start_date.strftime('%Y-%m-%d')
end_str = end_date.strftime('%Y-%m-%d') end_str = end_date.strftime('%Y-%m-%d')
@@ -1011,7 +1011,7 @@ async def update_user_subscription(
) )
subscription.end_date = request.end_date subscription.end_date = request.end_date
if request.end_date > datetime.utcnow(): if request.end_date > datetime.now(UTC):
subscription.status = SubscriptionStatus.ACTIVE.value subscription.status = SubscriptionStatus.ACTIVE.value
else: else:
subscription.status = SubscriptionStatus.EXPIRED.value subscription.status = SubscriptionStatus.EXPIRED.value
@@ -1107,7 +1107,7 @@ async def update_user_subscription(
if request.action == 'cancel': if request.action == 'cancel':
subscription.status = SubscriptionStatus.EXPIRED.value subscription.status = SubscriptionStatus.EXPIRED.value
subscription.end_date = datetime.utcnow() subscription.end_date = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(subscription) await db.refresh(subscription)
@@ -1124,9 +1124,9 @@ async def update_user_subscription(
if request.action == 'activate': if request.action == 'activate':
subscription.status = SubscriptionStatus.ACTIVE.value subscription.status = SubscriptionStatus.ACTIVE.value
if subscription.end_date and subscription.end_date <= datetime.utcnow(): if subscription.end_date and subscription.end_date <= datetime.now(UTC):
# Extend by 30 days if expired # Extend by 30 days if expired
subscription.end_date = datetime.utcnow() + timedelta(days=30) subscription.end_date = datetime.now(UTC) + timedelta(days=30)
await db.commit() await db.commit()
await db.refresh(subscription) await db.refresh(subscription)
@@ -1196,7 +1196,7 @@ async def update_user_subscription(
await db.delete(traffic_purchase) await db.delete(traffic_purchase)
# Recalculate traffic_reset_at from remaining active purchases # Recalculate traffic_reset_at from remaining active purchases
now = datetime.utcnow() now = datetime.now(UTC)
remaining_query = select(TrafficPurchase).where( remaining_query = select(TrafficPurchase).where(
TrafficPurchase.subscription_id == subscription.id, TrafficPurchase.subscription_id == subscription.id,
TrafficPurchase.expires_at > now, TrafficPurchase.expires_at > now,
@@ -1388,7 +1388,7 @@ async def update_user_status(
) )
user.status = new_status user.status = new_status
user.updated_at = datetime.utcnow() user.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(user) await db.refresh(user)
@@ -1456,7 +1456,7 @@ async def update_user_restrictions(
if request.restriction_reason is not None: if request.restriction_reason is not None:
user.restriction_reason = request.restriction_reason user.restriction_reason = request.restriction_reason
user.updated_at = datetime.utcnow() user.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(user) await db.refresh(user)
@@ -1511,7 +1511,7 @@ async def update_user_promo_group(
promo_group_name = promo_group.name promo_group_name = promo_group.name
user.promo_group_id = new_promo_group_id user.promo_group_id = new_promo_group_id
user.updated_at = datetime.utcnow() user.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(user) await db.refresh(user)
@@ -1552,7 +1552,7 @@ async def update_user_referral_commission(
old_commission = user.referral_commission_percent old_commission = user.referral_commission_percent
user.referral_commission_percent = request.commission_percent user.referral_commission_percent = request.commission_percent
user.updated_at = datetime.utcnow() user.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
logger.info( logger.info(
@@ -1837,7 +1837,7 @@ async def reset_user_trial(
# Reset trial flag # Reset trial flag
user.has_used_trial = False user.has_used_trial = False
user.updated_at = datetime.utcnow() user.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
@@ -1907,7 +1907,7 @@ async def reset_user_subscription(
await db.execute(delete(Subscription).where(Subscription.user_id == user_id)) await db.execute(delete(Subscription).where(Subscription.user_id == user_id))
subscription_deleted = True subscription_deleted = True
user.updated_at = datetime.utcnow() user.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
reason_text = f' (reason: {request.reason})' if request.reason else '' reason_text = f' (reason: {request.reason})' if request.reason else ''
@@ -1971,7 +1971,7 @@ async def disable_user(
# Block user account # Block user account
user.status = UserStatus.BLOCKED.value user.status = UserStatus.BLOCKED.value
user.updated_at = datetime.utcnow() user.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
reason_text = f' (reason: {request.reason})' if request.reason else '' reason_text = f' (reason: {request.reason})' if request.reason else ''
@@ -2179,13 +2179,8 @@ async def get_user_sync_status(
differences.append(f'Status: bot={bot_sub_status}, panel={panel_status}') differences.append(f'Status: bot={bot_sub_status}, panel={panel_status}')
if bot_sub_end_date and panel_expire_at: if bot_sub_end_date and panel_expire_at:
# Convert both to naive UTC for comparison bot_end_utc = bot_sub_end_date if bot_sub_end_date.tzinfo else bot_sub_end_date
# Bot dates are stored as naive UTC panel_end_utc = panel_datetime_to_utc(panel_expire_at)
bot_end_utc = (
bot_sub_end_date.replace(tzinfo=None) if bot_sub_end_date.tzinfo else bot_sub_end_date
)
# Panel returns local time with misleading +00:00 offset
panel_end_utc = panel_datetime_to_naive_utc(panel_expire_at)
diff_seconds = abs((bot_end_utc - panel_end_utc).total_seconds()) diff_seconds = abs((bot_end_utc - panel_end_utc).total_seconds())
# Allow for timezone offset (3 hours = MSK) and small sync delays # Allow for timezone offset (3 hours = MSK) and small sync delays
@@ -2319,7 +2314,7 @@ async def sync_user_from_panel(
short_uuid=panel_user.short_uuid, short_uuid=panel_user.short_uuid,
username=panel_user.username, username=panel_user.username,
status=panel_user.status.value if panel_user.status else None, status=panel_user.status.value if panel_user.status else None,
expire_at=panel_datetime_to_naive_utc(panel_user.expire_at) if panel_user.expire_at else None, expire_at=panel_datetime_to_utc(panel_user.expire_at) if panel_user.expire_at else None,
traffic_limit_gb=panel_user.traffic_limit_bytes / (1024**3) if panel_user.traffic_limit_bytes else 0, traffic_limit_gb=panel_user.traffic_limit_bytes / (1024**3) if panel_user.traffic_limit_bytes else 0,
traffic_used_gb=panel_user.used_traffic_bytes / (1024**3) if panel_user.used_traffic_bytes else 0, traffic_used_gb=panel_user.used_traffic_bytes / (1024**3) if panel_user.used_traffic_bytes else 0,
device_limit=panel_user.hwid_device_limit or 1, device_limit=panel_user.hwid_device_limit or 1,
@@ -2338,13 +2333,10 @@ async def sync_user_from_panel(
# Update end date (normalize timezone) # Update end date (normalize timezone)
if panel_user.expire_at: if panel_user.expire_at:
# Panel returns local time with misleading +00:00 offset panel_expire_utc = panel_datetime_to_utc(panel_user.expire_at)
panel_expire_utc = panel_datetime_to_naive_utc(panel_user.expire_at)
sub_end_naive = ( sub_end_utc = sub.end_date if sub.end_date and sub.end_date.tzinfo else sub.end_date
sub.end_date.replace(tzinfo=None) if sub.end_date and sub.end_date.tzinfo else sub.end_date if sub_end_utc != panel_expire_utc:
)
if sub_end_naive != panel_expire_utc:
changes['end_date'] = { changes['end_date'] = {
'old': sub.end_date.isoformat() if sub.end_date else None, 'old': sub.end_date.isoformat() if sub.end_date else None,
'new': panel_expire_utc.isoformat(), 'new': panel_expire_utc.isoformat(),
@@ -2353,7 +2345,7 @@ async def sync_user_from_panel(
# Update status # Update status
panel_status_str = panel_user.status.value if panel_user.status else 'DISABLED' panel_status_str = panel_user.status.value if panel_user.status else 'DISABLED'
now = datetime.utcnow() now = datetime.now(UTC)
# Compare with normalized panel expire date # Compare with normalized panel expire date
panel_expire_for_check = panel_expire_utc if panel_user.expire_at else None panel_expire_for_check = panel_expire_utc if panel_user.expire_at else None
if panel_status_str == 'ACTIVE' and panel_expire_for_check and panel_expire_for_check > now: if panel_status_str == 'ACTIVE' and panel_expire_for_check and panel_expire_for_check > now:
@@ -2410,9 +2402,8 @@ async def sync_user_from_panel(
panel_traffic_limit = ( panel_traffic_limit = (
int(panel_user.traffic_limit_bytes / (1024**3)) if panel_user.traffic_limit_bytes else 100 int(panel_user.traffic_limit_bytes / (1024**3)) if panel_user.traffic_limit_bytes else 100
) )
# Panel returns local time with misleading +00:00 offset panel_expire_utc = panel_datetime_to_utc(panel_user.expire_at)
panel_expire_naive = panel_datetime_to_naive_utc(panel_user.expire_at) days_remaining = max(1, (panel_expire_utc - datetime.now(UTC)).days)
days_remaining = max(1, (panel_expire_naive - datetime.utcnow()).days)
new_sub = await create_paid_subscription( new_sub = await create_paid_subscription(
db=db, db=db,
@@ -2427,8 +2418,8 @@ async def sync_user_from_panel(
changes['subscription_created'] = True changes['subscription_created'] = True
# Update last sync time # Update last sync time
user.last_remnawave_sync = datetime.utcnow() user.last_remnawave_sync = datetime.now(UTC)
user.updated_at = datetime.utcnow() user.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
@@ -2502,14 +2493,14 @@ async def sync_user_to_panel(
is_active = ( is_active = (
sub.status in (SubscriptionStatus.ACTIVE.value, SubscriptionStatus.TRIAL.value) sub.status in (SubscriptionStatus.ACTIVE.value, SubscriptionStatus.TRIAL.value)
and sub.end_date and sub.end_date
and sub.end_date > datetime.utcnow() and sub.end_date > datetime.now(UTC)
) )
panel_status = PanelUserStatus.ACTIVE if is_active else PanelUserStatus.DISABLED panel_status = PanelUserStatus.ACTIVE if is_active else PanelUserStatus.DISABLED
# Ensure expire_at is in future for panel # Ensure expire_at is in future for panel
expire_at = sub.end_date expire_at = sub.end_date
if expire_at and expire_at <= datetime.utcnow(): if expire_at and expire_at <= datetime.now(UTC):
expire_at = datetime.utcnow() + timedelta(minutes=1) expire_at = datetime.now(UTC) + timedelta(minutes=1)
username = settings.format_remnawave_username( username = settings.format_remnawave_username(
full_name=user.full_name, full_name=user.full_name,
@@ -2595,7 +2586,7 @@ async def sync_user_to_panel(
# Create new user in panel # Create new user in panel
create_kwargs = { create_kwargs = {
'username': username, 'username': username,
'expire_at': expire_at or (datetime.utcnow() + timedelta(days=30)), 'expire_at': expire_at or (datetime.now(UTC) + timedelta(days=30)),
'status': panel_status, 'status': panel_status,
'traffic_limit_bytes': traffic_limit_bytes, 'traffic_limit_bytes': traffic_limit_bytes,
'traffic_limit_strategy': TrafficLimitStrategy.MONTH, 'traffic_limit_strategy': TrafficLimitStrategy.MONTH,
@@ -2620,8 +2611,8 @@ async def sync_user_to_panel(
action = 'created' action = 'created'
# Update last sync time # Update last sync time
user.last_remnawave_sync = datetime.utcnow() user.last_remnawave_sync = datetime.now(UTC)
user.updated_at = datetime.utcnow() user.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
+11 -11
View File
@@ -25,7 +25,7 @@ from app.database.crud.user import (
from app.database.models import CabinetRefreshToken, User from app.database.models import CabinetRefreshToken, User
from app.services.disposable_email_service import disposable_email_service from app.services.disposable_email_service import disposable_email_service
from app.services.referral_service import process_referral_registration from app.services.referral_service import process_referral_registration
from app.utils.timezone import panel_datetime_to_naive_utc from app.utils.timezone import panel_datetime_to_utc
from ..auth import ( from ..auth import (
create_access_token, create_access_token,
@@ -175,7 +175,7 @@ async def _sync_subscription_from_panel_by_email(db: AsyncSession, user: User) -
existing_sub = await get_subscription_by_user_id(db, user.id) existing_sub = await get_subscription_by_user_id(db, user.id)
# Parse panel data — panel returns local time with misleading +00:00 offset # Parse panel data — panel returns local time with misleading +00:00 offset
expire_at = panel_datetime_to_naive_utc(panel_user.expire_at) expire_at = panel_datetime_to_utc(panel_user.expire_at)
traffic_limit_gb = panel_user.traffic_limit_bytes // (1024**3) if panel_user.traffic_limit_bytes > 0 else 0 traffic_limit_gb = panel_user.traffic_limit_bytes // (1024**3) if panel_user.traffic_limit_bytes > 0 else 0
traffic_used_gb = panel_user.used_traffic_bytes / (1024**3) if panel_user.used_traffic_bytes > 0 else 0 traffic_used_gb = panel_user.used_traffic_bytes / (1024**3) if panel_user.used_traffic_bytes > 0 else 0
@@ -186,7 +186,7 @@ async def _sync_subscription_from_panel_by_email(db: AsyncSession, user: User) -
device_limit = panel_user.hwid_device_limit or 1 device_limit = panel_user.hwid_device_limit or 1
# Determine status — expire_at is now naive UTC # Determine status — expire_at is now naive UTC
current_time = datetime.now(UTC).replace(tzinfo=None) current_time = datetime.now(UTC)
if panel_user.status.value == 'ACTIVE' and expire_at > current_time: if panel_user.status.value == 'ACTIVE' and expire_at > current_time:
sub_status = SubscriptionStatus.ACTIVE sub_status = SubscriptionStatus.ACTIVE
@@ -313,7 +313,7 @@ async def auth_telegram(
) )
# Update last login # Update last login
user.cabinet_last_login = datetime.utcnow() user.cabinet_last_login = datetime.now(UTC)
await db.commit() await db.commit()
response = _create_auth_response(user) response = _create_auth_response(user)
@@ -374,7 +374,7 @@ async def auth_telegram_widget(
if request.last_name != user.last_name: if request.last_name != user.last_name:
user.last_name = request.last_name user.last_name = request.last_name
user.cabinet_last_login = datetime.utcnow() user.cabinet_last_login = datetime.now(UTC)
await db.commit() await db.commit()
response = _create_auth_response(user) response = _create_auth_response(user)
@@ -546,7 +546,7 @@ async def register_email_standalone(
# Для тестового email - автоматически верифицировать # Для тестового email - автоматически верифицировать
if is_test_email: if is_test_email:
user.email_verified = True user.email_verified = True
user.email_verified_at = datetime.utcnow() user.email_verified_at = datetime.now(UTC)
await db.commit() await db.commit()
logger.info('Test email auto-verified: user_id', email=request.email, user_id=user.id) logger.info('Test email auto-verified: user_id', email=request.email, user_id=user.id)
else: else:
@@ -633,10 +633,10 @@ async def verify_email(
# Mark email as verified # Mark email as verified
user.email_verified = True user.email_verified = True
user.email_verified_at = datetime.utcnow() user.email_verified_at = datetime.now(UTC)
user.email_verification_token = None user.email_verification_token = None
user.email_verification_expires = None user.email_verification_expires = None
user.cabinet_last_login = datetime.utcnow() user.cabinet_last_login = datetime.now(UTC)
await db.commit() await db.commit()
@@ -750,7 +750,7 @@ async def login_email(
language='ru', language='ru',
) )
user.email_verified = True user.email_verified = True
user.email_verified_at = datetime.utcnow() user.email_verified_at = datetime.now(UTC)
await db.commit() await db.commit()
else: else:
raise HTTPException( raise HTTPException(
@@ -783,7 +783,7 @@ async def login_email(
detail='User account is not active', detail='User account is not active',
) )
user.cabinet_last_login = datetime.utcnow() user.cabinet_last_login = datetime.now(UTC)
await db.commit() await db.commit()
response = _create_auth_response(user) response = _create_auth_response(user)
@@ -871,7 +871,7 @@ async def logout(
token_record = result.scalar_one_or_none() token_record = result.scalar_one_or_none()
if token_record: if token_record:
token_record.revoked_at = datetime.utcnow() token_record.revoked_at = datetime.now(UTC)
await db.commit() await db.commit()
return {'message': 'Logged out successfully'} return {'message': 'Logged out successfully'}
+2 -2
View File
@@ -1,7 +1,7 @@
"""Contests routes for cabinet - user participation in games/contests.""" """Contests routes for cabinet - user participation in games/contests."""
import random import random
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
from typing import Any from typing import Any
import structlog import structlog
@@ -102,7 +102,7 @@ async def _award_prize(db: AsyncSession, user_id: int, prize_type: str, prize_va
return 'Error: subscription not found' return 'Error: subscription not found'
subscription.end_date = subscription.end_date + timedelta(days=days) subscription.end_date = subscription.end_date + timedelta(days=days)
subscription.updated_at = datetime.utcnow() subscription.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(subscription) await db.refresh(subscription)
+2 -2
View File
@@ -1,6 +1,6 @@
"""Notification settings routes for cabinet.""" """Notification settings routes for cabinet."""
from datetime import datetime from datetime import UTC, datetime
from typing import Any from typing import Any
import structlog import structlog
@@ -112,7 +112,7 @@ async def update_notification_settings(
user.notification_settings = {} user.notification_settings = {}
user.notification_settings = new_settings user.notification_settings = new_settings
user.updated_at = datetime.utcnow() user.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(user) await db.refresh(user)
+1 -1
View File
@@ -34,7 +34,7 @@ router = APIRouter(prefix='/auth/oauth', tags=['Cabinet OAuth'])
async def _finalize_oauth_login(db: AsyncSession, user: User, provider: str) -> AuthResponse: async def _finalize_oauth_login(db: AsyncSession, user: User, provider: str) -> AuthResponse:
"""Update last login, create tokens, store refresh token.""" """Update last login, create tokens, store refresh token."""
user.cabinet_last_login = datetime.now(UTC).replace(tzinfo=None) user.cabinet_last_login = datetime.now(UTC)
await db.commit() await db.commit()
auth_response = _create_auth_response(user) auth_response = _create_auth_response(user)
await _store_refresh_token(db, user.id, auth_response.refresh_token, device_info=f'oauth:{provider}') await _store_refresh_token(db, user.id, auth_response.refresh_token, device_info=f'oauth:{provider}')
+3 -3
View File
@@ -1,6 +1,6 @@
"""Polls routes for cabinet - user participation in polls/surveys.""" """Polls routes for cabinet - user participation in polls/surveys."""
from datetime import datetime from datetime import UTC, datetime
import structlog import structlog
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
@@ -247,7 +247,7 @@ async def start_poll(
# Mark as started if not already # Mark as started if not already
if not response.started_at: if not response.started_at:
response.started_at = datetime.utcnow() response.started_at = datetime.now(UTC)
await db.commit() await db.commit()
# Get next unanswered question # Get next unanswered question
@@ -346,7 +346,7 @@ async def answer_question(
) )
# Poll completed # Poll completed
response.completed_at = datetime.utcnow() response.completed_at = datetime.now(UTC)
await db.commit() await db.commit()
# Award reward if any # Award reward if any
+5 -5
View File
@@ -1,6 +1,6 @@
"""Promo offers routes for cabinet - personal discounts and offers.""" """Promo offers routes for cabinet - personal discounts and offers."""
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
from typing import Any from typing import Any
import structlog import structlog
@@ -112,7 +112,7 @@ async def get_promo_offers(
db: AsyncSession = Depends(get_cabinet_db), db: AsyncSession = Depends(get_cabinet_db),
): ):
"""Get list of available promo offers for the user.""" """Get list of available promo offers for the user."""
now = datetime.utcnow() now = datetime.now(UTC)
result = await db.execute( result = await db.execute(
select(DiscountOffer) select(DiscountOffer)
@@ -151,7 +151,7 @@ async def get_active_discount(
expires_at = user.promo_offer_discount_expires_at expires_at = user.promo_offer_discount_expires_at
source = user.promo_offer_discount_source source = user.promo_offer_discount_source
now = datetime.utcnow() now = datetime.now(UTC)
is_active = discount_percent > 0 and (expires_at is None or expires_at > now) is_active = discount_percent > 0 and (expires_at is None or expires_at > now)
return ActiveDiscountInfo( return ActiveDiscountInfo(
@@ -284,7 +284,7 @@ async def claim_promo_offer(
detail='Offer not found', detail='Offer not found',
) )
now = datetime.utcnow() now = datetime.now(UTC)
if offer.claimed_at is not None: if offer.claimed_at is not None:
raise HTTPException( raise HTTPException(
@@ -408,7 +408,7 @@ async def clear_active_discount(
user.promo_offer_discount_percent = 0 user.promo_offer_discount_percent = 0
user.promo_offer_discount_source = None user.promo_offer_discount_source = None
user.promo_offer_discount_expires_at = None user.promo_offer_discount_expires_at = None
user.updated_at = datetime.utcnow() user.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
+30 -30
View File
@@ -135,7 +135,7 @@ def _subscription_to_response(
traffic_purchases: list[dict[str, Any]] | None = None, traffic_purchases: list[dict[str, Any]] | None = None,
) -> SubscriptionData: ) -> SubscriptionData:
"""Convert Subscription model to response.""" """Convert Subscription model to response."""
now = datetime.utcnow() now = datetime.now(UTC)
# Use actual_status property for correct status (same as bot uses) # Use actual_status property for correct status (same as bot uses)
actual_status = subscription.actual_status actual_status = subscription.actual_status
@@ -276,7 +276,7 @@ async def get_subscription(
traffic_purchases_data = [] traffic_purchases_data = []
from app.database.models import TrafficPurchase from app.database.models import TrafficPurchase
now = datetime.utcnow() now = datetime.now(UTC)
purchases_query = ( purchases_query = (
select(TrafficPurchase) select(TrafficPurchase)
.where(TrafficPurchase.subscription_id == fresh_user.subscription.id) .where(TrafficPurchase.subscription_id == fresh_user.subscription.id)
@@ -515,7 +515,7 @@ async def renew_subscription(
user.promo_offer_discount_expires_at = None user.promo_offer_discount_expires_at = None
# Extend from end_date or now if expired # Extend from end_date or now if expired
now = datetime.utcnow() now = datetime.now(UTC)
if user.subscription.end_date and user.subscription.end_date > now: if user.subscription.end_date and user.subscription.end_date > now:
user.subscription.end_date = user.subscription.end_date + timedelta(days=request.period_days) user.subscription.end_date = user.subscription.end_date + timedelta(days=request.period_days)
else: else:
@@ -832,9 +832,9 @@ async def purchase_traffic(
# Устанавливаем дату сброса трафика (только при первой докупке) # Устанавливаем дату сброса трафика (только при первой докупке)
# При повторной докупке дата НЕ продлевается # При повторной докупке дата НЕ продлевается
if not subscription.traffic_reset_at: if not subscription.traffic_reset_at:
from datetime import timedelta from datetime import UTC, timedelta
subscription.traffic_reset_at = datetime.utcnow() + timedelta(days=30) subscription.traffic_reset_at = datetime.now(UTC) + timedelta(days=30)
logger.info( logger.info(
'Set traffic_reset_at for subscription', 'Set traffic_reset_at for subscription',
subscription_id=subscription.id, subscription_id=subscription.id,
@@ -1137,7 +1137,7 @@ async def get_trial_info(
# Check if user already has an active subscription # Check if user already has an active subscription
if user.subscription: if user.subscription:
now = datetime.utcnow() now = datetime.now(UTC)
is_active = ( is_active = (
user.subscription.status == 'active' and user.subscription.end_date and user.subscription.end_date > now user.subscription.status == 'active' and user.subscription.end_date and user.subscription.end_date > now
) )
@@ -1194,7 +1194,7 @@ async def activate_trial(
# Check if user already has an active subscription # Check if user already has an active subscription
if user.subscription: if user.subscription:
now = datetime.utcnow() now = datetime.now(UTC)
is_active = ( is_active = (
user.subscription.status == 'active' and user.subscription.end_date and user.subscription.end_date > now user.subscription.status == 'active' and user.subscription.end_date and user.subscription.end_date > now
) )
@@ -2010,7 +2010,7 @@ async def purchase_tariff(
# For daily tariffs, set last_daily_charge_at # For daily tariffs, set last_daily_charge_at
if is_daily_tariff: if is_daily_tariff:
subscription.last_daily_charge_at = datetime.utcnow() subscription.last_daily_charge_at = datetime.now(UTC)
subscription.is_daily_paused = False subscription.is_daily_paused = False
await db.commit() await db.commit()
await db.refresh(subscription) await db.refresh(subscription)
@@ -2088,7 +2088,7 @@ async def purchase_tariff(
try: try:
# Determine if this is a new subscription or extension # Determine if this is a new subscription or extension
was_new_subscription = ( was_new_subscription = (
subscription.start_date and (datetime.utcnow() - subscription.start_date).total_seconds() < 60 subscription.start_date and (datetime.now(UTC) - subscription.start_date).total_seconds() < 60
) )
notification_type = ( notification_type = (
NotificationType.SUBSCRIPTION_ACTIVATED NotificationType.SUBSCRIPTION_ACTIVATED
@@ -2124,7 +2124,7 @@ async def purchase_tariff(
notification_service = AdminNotificationService(bot) notification_service = AdminNotificationService(bot)
# Определяем тип покупки: новая подписка или продление # Определяем тип покупки: новая подписка или продление
was_new_subscription = ( was_new_subscription = (
subscription.start_date and (datetime.utcnow() - subscription.start_date).total_seconds() < 60 subscription.start_date and (datetime.now(UTC) - subscription.start_date).total_seconds() < 60
) )
await notification_service.send_subscription_purchase_notification( await notification_service.send_subscription_purchase_notification(
db=db, db=db,
@@ -2211,7 +2211,7 @@ async def purchase_devices(
) )
# Calculate prorated price based on remaining days # Calculate prorated price based on remaining days
from datetime import datetime from datetime import UTC, datetime
now = datetime.now(UTC) now = datetime.now(UTC)
end_date = subscription.end_date end_date = subscription.end_date
@@ -2627,7 +2627,7 @@ async def get_device_price(
} }
# Calculate prorated price # Calculate prorated price
from datetime import datetime from datetime import UTC, datetime
now = datetime.now(UTC) now = datetime.now(UTC)
end_date = subscription.end_date end_date = subscription.end_date
@@ -3079,9 +3079,9 @@ async def get_available_countries(
connected_squads = user.subscription.connected_squads or [] connected_squads = user.subscription.connected_squads or []
# Calculate days left for prorated pricing # Calculate days left for prorated pricing
if user.subscription.end_date: if user.subscription.end_date:
from datetime import datetime from datetime import UTC, datetime
delta = user.subscription.end_date - datetime.utcnow() delta = user.subscription.end_date - datetime.now(UTC)
days_left = max(0, delta.days) days_left = max(0, delta.days)
# Get discount from promo group # Get discount from promo group
@@ -3263,7 +3263,7 @@ async def update_countries(
# Update connected squads # Update connected squads
user.subscription.connected_squads = selected_countries user.subscription.connected_squads = selected_countries
user.subscription.updated_at = datetime.utcnow() user.subscription.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
# Sync with RemnaWave # Sync with RemnaWave
@@ -3910,7 +3910,7 @@ async def reduce_devices(
# Update subscription # Update subscription
subscription.device_limit = new_device_limit subscription.device_limit = new_device_limit
subscription.updated_at = datetime.utcnow() subscription.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
# Update RemnaWave # Update RemnaWave
@@ -4010,8 +4010,8 @@ async def preview_tariff_switch(
# Calculate remaining days # Calculate remaining days
remaining_days = 0 remaining_days = 0
if user.subscription.end_date and user.subscription.end_date > datetime.utcnow(): if user.subscription.end_date and user.subscription.end_date > datetime.now(UTC):
delta = user.subscription.end_date - datetime.utcnow() delta = user.subscription.end_date - datetime.now(UTC)
remaining_days = max(0, delta.days) remaining_days = max(0, delta.days)
# Calculate switch cost # Calculate switch cost
@@ -4128,7 +4128,7 @@ async def switch_tariff(
db: AsyncSession = Depends(get_cabinet_db), db: AsyncSession = Depends(get_cabinet_db),
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Switch to a different tariff without changing end date.""" """Switch to a different tariff without changing end date."""
from datetime import timedelta from datetime import UTC, timedelta
if not settings.is_tariffs_mode(): if not settings.is_tariffs_mode():
raise HTTPException( raise HTTPException(
@@ -4195,8 +4195,8 @@ async def switch_tariff(
# Calculate remaining days # Calculate remaining days
remaining_days = 0 remaining_days = 0
if user.subscription.end_date and user.subscription.end_date > datetime.utcnow(): if user.subscription.end_date and user.subscription.end_date > datetime.now(UTC):
delta = user.subscription.end_date - datetime.utcnow() delta = user.subscription.end_date - datetime.now(UTC)
remaining_days = max(0, delta.days) remaining_days = max(0, delta.days)
# Calculate cost # Calculate cost
@@ -4334,14 +4334,14 @@ async def switch_tariff(
if switching_to_daily: if switching_to_daily:
# Switching TO daily - reset end_date to 1 day, set last_daily_charge_at # Switching TO daily - reset end_date to 1 day, set last_daily_charge_at
user.subscription.end_date = datetime.utcnow() + timedelta(days=1) user.subscription.end_date = datetime.now(UTC) + timedelta(days=1)
user.subscription.last_daily_charge_at = datetime.utcnow() user.subscription.last_daily_charge_at = datetime.now(UTC)
user.subscription.is_daily_paused = False user.subscription.is_daily_paused = False
elif switching_from_daily: elif switching_from_daily:
user.subscription.end_date = datetime.utcnow() + timedelta(days=new_period_days) user.subscription.end_date = datetime.now(UTC) + timedelta(days=new_period_days)
user.subscription.is_daily_paused = False user.subscription.is_daily_paused = False
user.subscription.updated_at = datetime.utcnow() user.subscription.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
# Sync with RemnaWave # Sync with RemnaWave
@@ -4425,7 +4425,7 @@ async def toggle_subscription_pause(
db: AsyncSession = Depends(get_cabinet_db), db: AsyncSession = Depends(get_cabinet_db),
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Toggle pause/resume for daily subscription.""" """Toggle pause/resume for daily subscription."""
from datetime import timedelta from datetime import UTC, timedelta
await db.refresh(user, ['subscription']) await db.refresh(user, ['subscription'])
@@ -4476,8 +4476,8 @@ async def toggle_subscription_pause(
# Restore ACTIVE status if was DISABLED # Restore ACTIVE status if was DISABLED
if was_disabled: if was_disabled:
user.subscription.status = SubscriptionStatus.ACTIVE.value user.subscription.status = SubscriptionStatus.ACTIVE.value
user.subscription.last_daily_charge_at = datetime.utcnow() user.subscription.last_daily_charge_at = datetime.now(UTC)
user.subscription.end_date = datetime.utcnow() + timedelta(days=1) user.subscription.end_date = datetime.now(UTC) + timedelta(days=1)
await db.commit() await db.commit()
await db.refresh(user.subscription) await db.refresh(user.subscription)
@@ -4614,7 +4614,7 @@ async def switch_traffic_package(
user.subscription.traffic_limit_gb = new_traffic user.subscription.traffic_limit_gb = new_traffic
user.subscription.purchased_traffic_gb = 0 # Reset purchased traffic on switch user.subscription.purchased_traffic_gb = 0 # Reset purchased traffic on switch
user.subscription.traffic_reset_at = None # Reset traffic reset date user.subscription.traffic_reset_at = None # Reset traffic reset date
user.subscription.updated_at = datetime.utcnow() user.subscription.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
# Sync with RemnaWave # Sync with RemnaWave
@@ -4733,7 +4733,7 @@ async def refresh_traffic(
used_gb = traffic_stats.get('used_traffic_gb', 0) used_gb = traffic_stats.get('used_traffic_gb', 0)
if abs((user.subscription.traffic_used_gb or 0) - used_gb) > 0.01: if abs((user.subscription.traffic_used_gb or 0) - used_gb) > 0.01:
user.subscription.traffic_used_gb = used_gb user.subscription.traffic_used_gb = used_gb
user.subscription.updated_at = datetime.utcnow() user.subscription.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
# Calculate percentage # Calculate percentage
+6 -6
View File
@@ -1,7 +1,7 @@
"""Support tickets routes for cabinet.""" """Support tickets routes for cabinet."""
import math import math
from datetime import datetime from datetime import UTC, datetime
import structlog import structlog
from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi import APIRouter, Depends, HTTPException, Query, status
@@ -137,8 +137,8 @@ async def create_ticket(
title=request.title, title=request.title,
status='open', status='open',
priority='normal', priority='normal',
created_at=datetime.utcnow(), created_at=datetime.now(UTC),
updated_at=datetime.utcnow(), updated_at=datetime.now(UTC),
) )
db.add(ticket) db.add(ticket)
await db.flush() await db.flush()
@@ -152,7 +152,7 @@ async def create_ticket(
media_type=request.media_type, media_type=request.media_type,
media_file_id=request.media_file_id, media_file_id=request.media_file_id,
media_caption=request.media_caption, media_caption=request.media_caption,
created_at=datetime.utcnow(), created_at=datetime.now(UTC),
) )
db.add(message) db.add(message)
await db.commit() await db.commit()
@@ -268,14 +268,14 @@ async def add_ticket_message(
media_type=request.media_type, media_type=request.media_type,
media_file_id=request.media_file_id, media_file_id=request.media_file_id,
media_caption=request.media_caption, media_caption=request.media_caption,
created_at=datetime.utcnow(), created_at=datetime.now(UTC),
) )
db.add(message) db.add(message)
# Update ticket status and timestamp # Update ticket status and timestamp
if ticket.status == 'answered': if ticket.status == 'answered':
ticket.status = 'pending' ticket.status = 'pending'
ticket.updated_at = datetime.utcnow() ticket.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(message) await db.refresh(message)
@@ -4,7 +4,7 @@ Service for managing email template overrides stored in the database.
Custom templates override the hardcoded defaults from email_templates.py. Custom templates override the hardcoded defaults from email_templates.py.
""" """
from datetime import datetime from datetime import UTC, datetime
from typing import Any from typing import Any
import structlog import structlog
@@ -124,7 +124,7 @@ async def save_template_override(
) )
row = existing.fetchone() row = existing.fetchone()
now = datetime.utcnow() now = datetime.now(UTC)
if row: if row:
# Update # Update
+2 -2
View File
@@ -1,4 +1,4 @@
from datetime import datetime from datetime import UTC, datetime
import structlog import structlog
from sqlalchemy import and_, delete, func, select, update from sqlalchemy import and_, delete, func, select, update
@@ -151,7 +151,7 @@ async def update_campaign(
if not update_data: if not update_data:
return campaign return campaign
update_data['updated_at'] = datetime.utcnow() update_data['updated_at'] = datetime.now(UTC)
await db.execute(update(AdvertisingCampaign).where(AdvertisingCampaign.id == campaign.id).values(**update_data)) await db.execute(update(AdvertisingCampaign).where(AdvertisingCampaign.id == campaign.id).values(**update_data))
await db.commit() await db.commit()
+4 -4
View File
@@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime from datetime import UTC, datetime
from typing import Any from typing import Any
import structlog import structlog
@@ -127,7 +127,7 @@ async def update_cloudpayments_payment(
if hasattr(payment, key): if hasattr(payment, key):
setattr(payment, key, value) setattr(payment, key, value)
payment.updated_at = datetime.utcnow() payment.updated_at = datetime.now(UTC)
await db.flush() await db.flush()
await db.refresh(payment) await db.refresh(payment)
@@ -171,7 +171,7 @@ async def mark_cloudpayments_payment_as_paid(
payment.status = 'completed' payment.status = 'completed'
payment.is_paid = True payment.is_paid = True
payment.paid_at = datetime.utcnow() payment.paid_at = datetime.now(UTC)
if transaction_id_cp is not None: if transaction_id_cp is not None:
payment.transaction_id_cp = transaction_id_cp payment.transaction_id_cp = transaction_id_cp
@@ -190,7 +190,7 @@ async def mark_cloudpayments_payment_as_paid(
if callback_payload: if callback_payload:
payment.callback_payload = callback_payload payment.callback_payload = callback_payload
payment.updated_at = datetime.utcnow() payment.updated_at = datetime.now(UTC)
await db.flush() await db.flush()
await db.refresh(payment) await db.refresh(payment)
+3 -3
View File
@@ -1,5 +1,5 @@
from collections.abc import Sequence from collections.abc import Sequence
from datetime import datetime from datetime import UTC, datetime
import structlog import structlog
from sqlalchemy import and_, delete, desc, select from sqlalchemy import and_, delete, desc, select
@@ -107,7 +107,7 @@ async def create_round(
async def get_active_rounds(db: AsyncSession) -> list[ContestRound]: async def get_active_rounds(db: AsyncSession) -> list[ContestRound]:
now = datetime.utcnow() now = datetime.now(UTC)
result = await db.execute( result = await db.execute(
select(ContestRound) select(ContestRound)
.options(selectinload(ContestRound.template)) .options(selectinload(ContestRound.template))
@@ -124,7 +124,7 @@ async def get_active_rounds(db: AsyncSession) -> list[ContestRound]:
async def get_active_round_by_template(db: AsyncSession, template_id: int) -> ContestRound | None: async def get_active_round_by_template(db: AsyncSession, template_id: int) -> ContestRound | None:
now = datetime.utcnow() now = datetime.now(UTC)
result = await db.execute( result = await db.execute(
select(ContestRound) select(ContestRound)
.options(selectinload(ContestRound.template)) .options(selectinload(ContestRound.template))
+5 -5
View File
@@ -1,4 +1,4 @@
from datetime import datetime from datetime import UTC, datetime
import structlog import structlog
from sqlalchemy import and_, select from sqlalchemy import and_, select
@@ -76,7 +76,7 @@ async def update_cryptobot_payment_status(
return None return None
payment.status = status payment.status = status
payment.updated_at = datetime.utcnow() payment.updated_at = datetime.now(UTC)
if status == 'paid' and paid_at: if status == 'paid' and paid_at:
payment.paid_at = paid_at payment.paid_at = paid_at
@@ -97,7 +97,7 @@ async def link_cryptobot_payment_to_transaction(
return None return None
payment.transaction_id = transaction_id payment.transaction_id = transaction_id
payment.updated_at = datetime.utcnow() payment.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(payment) await db.refresh(payment)
@@ -120,9 +120,9 @@ async def get_user_cryptobot_payments(
async def get_pending_cryptobot_payments(db: AsyncSession, older_than_hours: int = 24) -> list[CryptoBotPayment]: async def get_pending_cryptobot_payments(db: AsyncSession, older_than_hours: int = 24) -> list[CryptoBotPayment]:
from datetime import timedelta from datetime import UTC, timedelta
cutoff_time = datetime.utcnow() - timedelta(hours=older_than_hours) cutoff_time = datetime.now(UTC) - timedelta(hours=older_than_hours)
result = await db.execute( result = await db.execute(
select(CryptoBotPayment) select(CryptoBotPayment)
+5 -5
View File
@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
import structlog import structlog
from sqlalchemy import func, select from sqlalchemy import func, select
@@ -28,7 +28,7 @@ async def upsert_discount_offer(
) -> DiscountOffer: ) -> DiscountOffer:
"""Create or refresh a discount offer for a user.""" """Create or refresh a discount offer for a user."""
expires_at = datetime.utcnow() + timedelta(hours=valid_hours) expires_at = datetime.now(UTC) + timedelta(hours=valid_hours)
result = await db.execute( result = await db.execute(
select(DiscountOffer) select(DiscountOffer)
@@ -116,7 +116,7 @@ async def list_active_discount_offers_for_user(
) -> list[DiscountOffer]: ) -> list[DiscountOffer]:
"""Return active (not yet claimed) offers for a user.""" """Return active (not yet claimed) offers for a user."""
now = datetime.utcnow() now = datetime.now(UTC)
stmt = ( stmt = (
select(DiscountOffer) select(DiscountOffer)
.options( .options(
@@ -161,7 +161,7 @@ async def mark_offer_claimed(
*, *,
details: dict | None = None, details: dict | None = None,
) -> DiscountOffer: ) -> DiscountOffer:
offer.claimed_at = datetime.utcnow() offer.claimed_at = datetime.now(UTC)
offer.is_active = False offer.is_active = False
await db.commit() await db.commit()
await db.refresh(offer) await db.refresh(offer)
@@ -190,7 +190,7 @@ async def mark_offer_claimed(
async def deactivate_expired_offers(db: AsyncSession) -> int: async def deactivate_expired_offers(db: AsyncSession) -> int:
now = datetime.utcnow() now = datetime.now(UTC)
result = await db.execute( result = await db.execute(
select(DiscountOffer).where( select(DiscountOffer).where(
DiscountOffer.is_active == True, DiscountOffer.is_active == True,
+4 -4
View File
@@ -1,5 +1,5 @@
from collections.abc import Iterable from collections.abc import Iterable
from datetime import datetime from datetime import UTC, datetime
import structlog import structlog
from sqlalchemy import delete, func, select, update from sqlalchemy import delete, func, select, update
@@ -21,7 +21,7 @@ async def set_faq_enabled(db: AsyncSession, language: str, enabled: bool) -> Faq
if setting: if setting:
setting.is_enabled = bool(enabled) setting.is_enabled = bool(enabled)
setting.updated_at = datetime.utcnow() setting.updated_at = datetime.now(UTC)
else: else:
setting = FaqSetting( setting = FaqSetting(
language=language, language=language,
@@ -117,7 +117,7 @@ async def update_faq_page(
if is_active is not None: if is_active is not None:
page.is_active = bool(is_active) page.is_active = bool(is_active)
page.updated_at = datetime.utcnow() page.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(page) await db.refresh(page)
@@ -139,6 +139,6 @@ async def bulk_update_order(
) -> None: ) -> None:
for page_id, order in pages: for page_id, order in pages:
await db.execute( await db.execute(
update(FaqPage).where(FaqPage.id == page_id).values(display_order=order, updated_at=datetime.utcnow()) update(FaqPage).where(FaqPage.id == page_id).values(display_order=order, updated_at=datetime.now(UTC))
) )
await db.commit() await db.commit()
+4 -4
View File
@@ -1,7 +1,7 @@
"""CRUD операции для платежей Freekassa.""" """CRUD операции для платежей Freekassa."""
import json import json
from datetime import datetime from datetime import UTC, datetime
import structlog import structlog
from sqlalchemy import select from sqlalchemy import select
@@ -77,10 +77,10 @@ async def update_freekassa_payment_status(
"""Обновляет статус платежа.""" """Обновляет статус платежа."""
payment.status = status payment.status = status
payment.is_paid = is_paid payment.is_paid = is_paid
payment.updated_at = datetime.utcnow() payment.updated_at = datetime.now(UTC)
if is_paid: if is_paid:
payment.paid_at = datetime.utcnow() payment.paid_at = datetime.now(UTC)
if freekassa_order_id: if freekassa_order_id:
payment.freekassa_order_id = freekassa_order_id payment.freekassa_order_id = freekassa_order_id
if payment_system_id is not None: if payment_system_id is not None:
@@ -134,7 +134,7 @@ async def get_expired_pending_payments(
db: AsyncSession, db: AsyncSession,
) -> list[FreekassaPayment]: ) -> list[FreekassaPayment]:
"""Получает просроченные платежи в статусе pending.""" """Получает просроченные платежи в статусе pending."""
now = datetime.utcnow() now = datetime.now(UTC)
result = await db.execute( result = await db.execute(
select(FreekassaPayment).where( select(FreekassaPayment).where(
FreekassaPayment.status == 'pending', FreekassaPayment.status == 'pending',
+3 -3
View File
@@ -1,4 +1,4 @@
from datetime import datetime from datetime import UTC, datetime
from typing import Any from typing import Any
import structlog import structlog
@@ -129,7 +129,7 @@ async def update_heleket_payment(
if paid_at is not None: if paid_at is not None:
payment.paid_at = paid_at payment.paid_at = paid_at
payment.updated_at = datetime.utcnow() payment.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(payment) await db.refresh(payment)
@@ -157,7 +157,7 @@ async def link_heleket_payment_to_transaction(
return None return None
payment.transaction_id = transaction_id payment.transaction_id = transaction_id
payment.updated_at = datetime.utcnow() payment.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(payment) await db.refresh(payment)
+4 -4
View File
@@ -1,7 +1,7 @@
"""CRUD операции для платежей KassaAI.""" """CRUD операции для платежей KassaAI."""
import json import json
from datetime import datetime from datetime import UTC, datetime
import structlog import structlog
from sqlalchemy import select from sqlalchemy import select
@@ -79,10 +79,10 @@ async def update_kassa_ai_payment_status(
"""Обновляет статус платежа.""" """Обновляет статус платежа."""
payment.status = status payment.status = status
payment.is_paid = is_paid payment.is_paid = is_paid
payment.updated_at = datetime.utcnow() payment.updated_at = datetime.now(UTC)
if is_paid: if is_paid:
payment.paid_at = datetime.utcnow() payment.paid_at = datetime.now(UTC)
if kassa_ai_order_id: if kassa_ai_order_id:
payment.kassa_ai_order_id = kassa_ai_order_id payment.kassa_ai_order_id = kassa_ai_order_id
if payment_system_id is not None: if payment_system_id is not None:
@@ -136,7 +136,7 @@ async def get_expired_pending_kassa_ai_payments(
db: AsyncSession, db: AsyncSession,
) -> list[KassaAiPayment]: ) -> list[KassaAiPayment]:
"""Получает просроченные платежи в статусе pending.""" """Получает просроченные платежи в статусе pending."""
now = datetime.utcnow() now = datetime.now(UTC)
result = await db.execute( result = await db.execute(
select(KassaAiPayment).where( select(KassaAiPayment).where(
KassaAiPayment.status == 'pending', KassaAiPayment.status == 'pending',
+4 -4
View File
@@ -1,4 +1,4 @@
from datetime import datetime from datetime import UTC, datetime
import structlog import structlog
from sqlalchemy import select from sqlalchemy import select
@@ -90,7 +90,7 @@ async def update_mulenpay_payment_status(
if metadata is not None: if metadata is not None:
payment.metadata_json = metadata payment.metadata_json = metadata
payment.updated_at = datetime.utcnow() payment.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(payment) await db.refresh(payment)
return payment return payment
@@ -103,7 +103,7 @@ async def update_mulenpay_payment_metadata(
metadata: dict, metadata: dict,
) -> MulenPayPayment: ) -> MulenPayPayment:
payment.metadata_json = metadata payment.metadata_json = metadata
payment.updated_at = datetime.utcnow() payment.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(payment) await db.refresh(payment)
return payment return payment
@@ -116,7 +116,7 @@ async def link_mulenpay_payment_to_transaction(
transaction_id: int, transaction_id: int,
) -> MulenPayPayment: ) -> MulenPayPayment:
payment.transaction_id = transaction_id payment.transaction_id = transaction_id
payment.updated_at = datetime.utcnow() payment.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(payment) await db.refresh(payment)
return payment return payment
+3 -3
View File
@@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime from datetime import UTC, datetime
from typing import Any from typing import Any
import structlog import structlog
@@ -115,7 +115,7 @@ async def update_platega_payment(
if expires_at is not None: if expires_at is not None:
payment.expires_at = expires_at payment.expires_at = expires_at
payment.updated_at = datetime.utcnow() payment.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(payment) await db.refresh(payment)
@@ -129,7 +129,7 @@ async def link_platega_payment_to_transaction(
transaction_id: int, transaction_id: int,
) -> PlategaPayment: ) -> PlategaPayment:
payment.transaction_id = transaction_id payment.transaction_id = transaction_id
payment.updated_at = datetime.utcnow() payment.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(payment) await db.refresh(payment)
return payment return payment
+3 -3
View File
@@ -1,4 +1,4 @@
from datetime import datetime from datetime import UTC, datetime
import structlog import structlog
from sqlalchemy import select from sqlalchemy import select
@@ -26,7 +26,7 @@ async def upsert_privacy_policy(
if policy: if policy:
policy.content = content or '' policy.content = content or ''
policy.updated_at = datetime.utcnow() policy.updated_at = datetime.now(UTC)
else: else:
policy = PrivacyPolicy( policy = PrivacyPolicy(
language=language, language=language,
@@ -52,7 +52,7 @@ async def set_privacy_policy_enabled(
if policy: if policy:
policy.is_enabled = bool(enabled) policy.is_enabled = bool(enabled)
policy.updated_at = datetime.utcnow() policy.updated_at = datetime.now(UTC)
else: else:
policy = PrivacyPolicy( policy = PrivacyPolicy(
language=language, language=language,
+4 -4
View File
@@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Iterable from collections.abc import Iterable
from datetime import datetime from datetime import UTC, datetime
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -118,13 +118,13 @@ async def ensure_default_templates(db: AsyncSession, *, created_by: int | None =
if should_update and new_message: if should_update and new_message:
existing.message_text = new_message existing.message_text = new_message
existing.updated_at = datetime.utcnow() existing.updated_at = datetime.now(UTC)
await db.flush() await db.flush()
target_active_hours = template_data.get('active_discount_hours') target_active_hours = template_data.get('active_discount_hours')
if target_active_hours is not None and target_active_hours > 0 and not existing.active_discount_hours: if target_active_hours is not None and target_active_hours > 0 and not existing.active_discount_hours:
existing.active_discount_hours = target_active_hours existing.active_discount_hours = target_active_hours
existing.updated_at = datetime.utcnow() existing.updated_at = datetime.now(UTC)
await db.flush() await db.flush()
templates.append(existing) templates.append(existing)
continue continue
@@ -204,7 +204,7 @@ async def update_promo_offer_template(
if is_active is not None: if is_active is not None:
template.is_active = is_active template.is_active = is_active
template.updated_at = datetime.utcnow() template.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(template) await db.refresh(template)
+6 -6
View File
@@ -1,4 +1,4 @@
from datetime import datetime from datetime import UTC, datetime
import structlog import structlog
from sqlalchemy import and_, func, select from sqlalchemy import and_, func, select
@@ -114,7 +114,7 @@ async def check_user_promocode_usage(db: AsyncSession, user_id: int, promocode_i
async def create_promocode_use(db: AsyncSession, promocode_id: int, user_id: int) -> PromoCodeUse: async def create_promocode_use(db: AsyncSession, promocode_id: int, user_id: int) -> PromoCodeUse:
promocode_use = PromoCodeUse(promocode_id=promocode_id, user_id=user_id, used_at=datetime.utcnow()) promocode_use = PromoCodeUse(promocode_id=promocode_id, user_id=user_id, used_at=datetime.now(UTC))
db.add(promocode_use) db.add(promocode_use)
await db.commit() await db.commit()
@@ -133,9 +133,9 @@ async def get_promocode_use_by_user_and_code(db: AsyncSession, user_id: int, pro
async def count_user_recent_activations(db: AsyncSession, user_id: int, hours: int = 24) -> int: async def count_user_recent_activations(db: AsyncSession, user_id: int, hours: int = 24) -> int:
"""Подсчитывает количество активаций промокодов пользователем за последние N часов.""" """Подсчитывает количество активаций промокодов пользователем за последние N часов."""
from datetime import timedelta from datetime import UTC, timedelta
cutoff = datetime.utcnow() - timedelta(hours=hours) cutoff = datetime.now(UTC) - timedelta(hours=hours)
result = await db.execute( result = await db.execute(
select(func.count(PromoCodeUse.id)).where(and_(PromoCodeUse.user_id == user_id, PromoCodeUse.used_at >= cutoff)) select(func.count(PromoCodeUse.id)).where(and_(PromoCodeUse.user_id == user_id, PromoCodeUse.used_at >= cutoff))
) )
@@ -178,7 +178,7 @@ async def update_promocode(db: AsyncSession, promocode: PromoCode, **kwargs) ->
if hasattr(promocode, field): if hasattr(promocode, field):
setattr(promocode, field, value) setattr(promocode, field, value)
promocode.updated_at = datetime.utcnow() promocode.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(promocode) await db.refresh(promocode)
@@ -239,7 +239,7 @@ async def get_promocode_statistics(db: AsyncSession, promocode_id: int) -> dict:
) )
total_uses = total_uses_result.scalar() total_uses = total_uses_result.scalar()
today = datetime.utcnow().date() today = datetime.now(UTC).date()
today_uses_result = await db.execute( today_uses_result = await db.execute(
select(func.count(PromoCodeUse.id)).where( select(func.count(PromoCodeUse.id)).where(
and_(PromoCodeUse.promocode_id == promocode_id, PromoCodeUse.used_at >= today) and_(PromoCodeUse.promocode_id == promocode_id, PromoCodeUse.used_at >= today)
+3 -3
View File
@@ -1,4 +1,4 @@
from datetime import datetime from datetime import UTC, datetime
import structlog import structlog
from sqlalchemy import select from sqlalchemy import select
@@ -26,7 +26,7 @@ async def upsert_public_offer(
if offer: if offer:
offer.content = content or '' offer.content = content or ''
offer.updated_at = datetime.utcnow() offer.updated_at = datetime.now(UTC)
else: else:
offer = PublicOffer( offer = PublicOffer(
language=language, language=language,
@@ -52,7 +52,7 @@ async def set_public_offer_enabled(
if offer: if offer:
offer.is_enabled = bool(enabled) offer.is_enabled = bool(enabled)
offer.updated_at = datetime.utcnow() offer.updated_at = datetime.now(UTC)
else: else:
offer = PublicOffer( offer = PublicOffer(
language=language, language=language,
+7 -7
View File
@@ -1,4 +1,4 @@
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
import structlog import structlog
from sqlalchemy import and_, func, select from sqlalchemy import and_, func, select
@@ -178,7 +178,7 @@ async def get_referral_statistics(db: AsyncSession) -> dict:
} }
) )
today = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) today = datetime.now(UTC).replace(hour=0, minute=0, second=0, microsecond=0)
today_referral_earnings_result = await db.execute( today_referral_earnings_result = await db.execute(
select(func.coalesce(func.sum(ReferralEarning.amount_kopeks), 0)).where(ReferralEarning.created_at >= today) select(func.coalesce(func.sum(ReferralEarning.amount_kopeks), 0)).where(ReferralEarning.created_at >= today)
@@ -190,7 +190,7 @@ async def get_referral_statistics(db: AsyncSession) -> dict:
) )
today_earnings = today_referral_earnings_result.scalar() + today_transaction_earnings_result.scalar() today_earnings = today_referral_earnings_result.scalar() + today_transaction_earnings_result.scalar()
week_ago = datetime.utcnow() - timedelta(days=7) week_ago = datetime.now(UTC) - timedelta(days=7)
week_referral_earnings_result = await db.execute( week_referral_earnings_result = await db.execute(
select(func.coalesce(func.sum(ReferralEarning.amount_kopeks), 0)).where(ReferralEarning.created_at >= week_ago) select(func.coalesce(func.sum(ReferralEarning.amount_kopeks), 0)).where(ReferralEarning.created_at >= week_ago)
) )
@@ -201,7 +201,7 @@ async def get_referral_statistics(db: AsyncSession) -> dict:
) )
week_earnings = week_referral_earnings_result.scalar() + week_transaction_earnings_result.scalar() week_earnings = week_referral_earnings_result.scalar() + week_transaction_earnings_result.scalar()
month_ago = datetime.utcnow() - timedelta(days=30) month_ago = datetime.now(UTC) - timedelta(days=30)
month_referral_earnings_result = await db.execute( month_referral_earnings_result = await db.execute(
select(func.coalesce(func.sum(ReferralEarning.amount_kopeks), 0)).where(ReferralEarning.created_at >= month_ago) select(func.coalesce(func.sum(ReferralEarning.amount_kopeks), 0)).where(ReferralEarning.created_at >= month_ago)
) )
@@ -249,7 +249,7 @@ async def get_top_referrers_by_period(
""" """
from app.database.models import Transaction, TransactionType from app.database.models import Transaction, TransactionType
now = datetime.utcnow() now = datetime.now(UTC)
if period == 'week': if period == 'week':
start_date = now - timedelta(days=7) start_date = now - timedelta(days=7)
else: # month else: # month
@@ -380,12 +380,12 @@ async def get_user_referral_stats(db: AsyncSession, user_id: int) -> dict:
total_earned = await get_referral_earnings_sum(db, user_id) total_earned = await get_referral_earnings_sum(db, user_id)
month_ago = datetime.utcnow() - timedelta(days=30) month_ago = datetime.now(UTC) - timedelta(days=30)
month_earned = await get_referral_earnings_sum(db, user_id, start_date=month_ago) month_earned = await get_referral_earnings_sum(db, user_id, start_date=month_ago)
from app.database.models import Subscription, SubscriptionStatus from app.database.models import Subscription, SubscriptionStatus
current_time = datetime.utcnow() current_time = datetime.now(UTC)
active_referrals_result = await db.execute( active_referrals_result = await db.execute(
select(func.count(User.id)) select(func.count(User.id))
+3 -3
View File
@@ -1,5 +1,5 @@
from collections.abc import Sequence from collections.abc import Sequence
from datetime import date, datetime, time from datetime import UTC, date, datetime, time
import structlog import structlog
from sqlalchemy import and_, desc, func, select from sqlalchemy import and_, desc, func, select
@@ -165,7 +165,7 @@ async def add_contest_event(
referral_id=referral_id, referral_id=referral_id,
amount_kopeks=amount_kopeks, amount_kopeks=amount_kopeks,
event_type=event_type, event_type=event_type,
occurred_at=datetime.utcnow(), occurred_at=datetime.now(UTC),
) )
db.add(event) db.add(event)
await db.commit() await db.commit()
@@ -512,7 +512,7 @@ async def upsert_contest_event(
referral_id=referral_id, referral_id=referral_id,
amount_kopeks=amount_kopeks, amount_kopeks=amount_kopeks,
event_type=event_type, event_type=event_type,
occurred_at=datetime.utcnow(), occurred_at=datetime.now(UTC),
) )
db.add(event) db.add(event)
await db.commit() await db.commit()
+4 -4
View File
@@ -1,4 +1,4 @@
from datetime import datetime from datetime import UTC, datetime
import structlog import structlog
from sqlalchemy import select, update from sqlalchemy import select, update
@@ -30,7 +30,7 @@ async def create_or_update_rules(
for rule in existing_rules: for rule in existing_rules:
rule.is_active = False rule.is_active = False
rule.updated_at = datetime.utcnow() rule.updated_at = datetime.now(UTC)
new_rules = ServiceRule(title=title, content=content, language=language, is_active=True, order=0) new_rules = ServiceRule(title=title, content=content, language=language, is_active=True, order=0)
@@ -47,7 +47,7 @@ async def clear_all_rules(db: AsyncSession, language: str = 'ru') -> bool:
result = await db.execute( result = await db.execute(
update(ServiceRule) update(ServiceRule)
.where(ServiceRule.language == language, ServiceRule.is_active == True) .where(ServiceRule.language == language, ServiceRule.is_active == True)
.values(is_active=False, updated_at=datetime.utcnow()) .values(is_active=False, updated_at=datetime.now(UTC))
) )
await db.commit() await db.commit()
@@ -110,7 +110,7 @@ async def restore_rules_version(db: AsyncSession, rule_id: int, language: str =
await db.execute( await db.execute(
update(ServiceRule) update(ServiceRule)
.where(ServiceRule.language == language, ServiceRule.is_active == True) .where(ServiceRule.language == language, ServiceRule.is_active == True)
.values(is_active=False, updated_at=datetime.utcnow()) .values(is_active=False, updated_at=datetime.now(UTC))
) )
restored_rule = ServiceRule( restored_rule = ServiceRule(
+3 -3
View File
@@ -1,6 +1,6 @@
import random import random
from collections.abc import Iterable, Sequence from collections.abc import Iterable, Sequence
from datetime import datetime from datetime import UTC, datetime
import structlog import structlog
from sqlalchemy import ( from sqlalchemy import (
@@ -360,7 +360,7 @@ async def sync_with_remnawave(db: AsyncSession, remnawave_squads: list[dict]) ->
if len(filtered_squads) != len(current_squads): if len(filtered_squads) != len(current_squads):
subscription.connected_squads = filtered_squads subscription.connected_squads = filtered_squads
subscription.updated_at = datetime.utcnow() subscription.updated_at = datetime.now(UTC)
cleaned_subscriptions += 1 cleaned_subscriptions += 1
# Clean up stale UUIDs from tariff allowed_squads # Clean up stale UUIDs from tariff allowed_squads
@@ -373,7 +373,7 @@ async def sync_with_remnawave(db: AsyncSession, remnawave_squads: list[dict]) ->
filtered = [u for u in current if u not in removed_uuids] filtered = [u for u in current if u not in removed_uuids]
if len(filtered) != len(current): if len(filtered) != len(current):
tariff.allowed_squads = filtered tariff.allowed_squads = filtered
tariff.updated_at = datetime.utcnow() tariff.updated_at = datetime.now(UTC)
cleaned_tariffs += 1 cleaned_tariffs += 1
logger.info( logger.info(
'🧹 Тариф "%s" (ID: %s): удалены несуществующие сквады %s', '🧹 Тариф "%s" (ID: %s): удалены несуществующие сквады %s',
+39 -39
View File
@@ -31,7 +31,7 @@ def is_recently_updated_by_webhook(subscription: Subscription) -> bool:
"""Return True if subscription was updated by webhook within guard window.""" """Return True if subscription was updated by webhook within guard window."""
if not subscription.last_webhook_update_at: if not subscription.last_webhook_update_at:
return False return False
elapsed = (datetime.now(UTC).replace(tzinfo=None) - subscription.last_webhook_update_at).total_seconds() elapsed = (datetime.now(UTC) - subscription.last_webhook_update_at).total_seconds()
return elapsed < _WEBHOOK_GUARD_SECONDS return elapsed < _WEBHOOK_GUARD_SECONDS
@@ -101,13 +101,13 @@ async def create_trial_subscription(
except Exception as error: except Exception as error:
logger.error('Не удалось получить сквад для триальной подписки пользователя', user_id=user_id, error=error) logger.error('Не удалось получить сквад для триальной подписки пользователя', user_id=user_id, error=error)
end_date = datetime.utcnow() + timedelta(days=duration_days) end_date = datetime.now(UTC) + timedelta(days=duration_days)
# Check for existing PENDING trial subscription (retry after failed payment) # Check for existing PENDING trial subscription (retry after failed payment)
existing = await get_subscription_by_user_id(db, user_id) existing = await get_subscription_by_user_id(db, user_id)
if existing and existing.is_trial and existing.status == SubscriptionStatus.PENDING.value: if existing and existing.is_trial and existing.status == SubscriptionStatus.PENDING.value:
existing.status = SubscriptionStatus.ACTIVE.value existing.status = SubscriptionStatus.ACTIVE.value
existing.start_date = datetime.utcnow() existing.start_date = datetime.now(UTC)
existing.end_date = end_date existing.end_date = end_date
existing.traffic_limit_gb = traffic_limit_gb existing.traffic_limit_gb = traffic_limit_gb
existing.device_limit = device_limit existing.device_limit = device_limit
@@ -124,7 +124,7 @@ async def create_trial_subscription(
user_id=user_id, user_id=user_id,
status=SubscriptionStatus.ACTIVE.value, status=SubscriptionStatus.ACTIVE.value,
is_trial=True, is_trial=True,
start_date=datetime.utcnow(), start_date=datetime.now(UTC),
end_date=end_date, end_date=end_date,
traffic_limit_gb=traffic_limit_gb, traffic_limit_gb=traffic_limit_gb,
device_limit=device_limit, device_limit=device_limit,
@@ -176,7 +176,7 @@ async def create_paid_subscription(
is_trial: bool = False, is_trial: bool = False,
tariff_id: int | None = None, tariff_id: int | None = None,
) -> Subscription: ) -> Subscription:
end_date = datetime.utcnow() + timedelta(days=duration_days) end_date = datetime.now(UTC) + timedelta(days=duration_days)
if device_limit is None: if device_limit is None:
device_limit = settings.DEFAULT_DEVICE_LIMIT device_limit = settings.DEFAULT_DEVICE_LIMIT
@@ -185,7 +185,7 @@ async def create_paid_subscription(
user_id=user_id, user_id=user_id,
status=SubscriptionStatus.ACTIVE.value, status=SubscriptionStatus.ACTIVE.value,
is_trial=is_trial, is_trial=is_trial,
start_date=datetime.utcnow(), start_date=datetime.now(UTC),
end_date=end_date, end_date=end_date,
traffic_limit_gb=traffic_limit_gb, traffic_limit_gb=traffic_limit_gb,
device_limit=device_limit, device_limit=device_limit,
@@ -253,7 +253,7 @@ async def replace_subscription(
) -> Subscription: ) -> Subscription:
"""Перезаписывает параметры существующей подписки пользователя.""" """Перезаписывает параметры существующей подписки пользователя."""
current_time = datetime.utcnow() current_time = datetime.now(UTC)
old_squads = set(subscription.connected_squads or []) old_squads = set(subscription.connected_squads or [])
new_squads = set(connected_squads or []) new_squads = set(connected_squads or [])
@@ -341,7 +341,7 @@ async def extend_subscription(
device_limit: Лимит устройств (опционально, для режима тарифов) device_limit: Лимит устройств (опционально, для режима тарифов)
connected_squads: Список UUID сквадов (опционально, для режима тарифов) connected_squads: Список UUID сквадов (опционально, для режима тарифов)
""" """
current_time = datetime.utcnow() current_time = datetime.now(UTC)
logger.info('🔄 Продление подписки на дней', subscription_id=subscription.id, days=days) logger.info('🔄 Продление подписки на дней', subscription_id=subscription.id, days=days)
logger.info( logger.info(
@@ -535,16 +535,16 @@ async def extend_subscription(
async def add_subscription_traffic(db: AsyncSession, subscription: Subscription, gb: int) -> Subscription: async def add_subscription_traffic(db: AsyncSession, subscription: Subscription, gb: int) -> Subscription:
subscription.add_traffic(gb) subscription.add_traffic(gb)
subscription.updated_at = datetime.utcnow() subscription.updated_at = datetime.now(UTC)
# Создаём новую запись докупки с индивидуальной датой истечения (30 дней) # Создаём новую запись докупки с индивидуальной датой истечения (30 дней)
from datetime import timedelta from datetime import UTC, timedelta
from sqlalchemy import select as sql_select from sqlalchemy import select as sql_select
from app.database.models import TrafficPurchase from app.database.models import TrafficPurchase
new_expires_at = datetime.utcnow() + timedelta(days=30) new_expires_at = datetime.now(UTC) + timedelta(days=30)
new_purchase = TrafficPurchase(subscription_id=subscription.id, traffic_gb=gb, expires_at=new_expires_at) new_purchase = TrafficPurchase(subscription_id=subscription.id, traffic_gb=gb, expires_at=new_expires_at)
db.add(new_purchase) db.add(new_purchase)
@@ -553,7 +553,7 @@ async def add_subscription_traffic(db: AsyncSession, subscription: Subscription,
subscription.purchased_traffic_gb = current_purchased + gb subscription.purchased_traffic_gb = current_purchased + gb
# Устанавливаем traffic_reset_at на ближайшую дату истечения из всех активных докупок # Устанавливаем traffic_reset_at на ближайшую дату истечения из всех активных докупок
now = datetime.utcnow() now = datetime.now(UTC)
active_purchases_query = ( active_purchases_query = (
sql_select(TrafficPurchase) sql_select(TrafficPurchase)
.where(TrafficPurchase.subscription_id == subscription.id) .where(TrafficPurchase.subscription_id == subscription.id)
@@ -585,7 +585,7 @@ async def add_subscription_traffic(db: AsyncSession, subscription: Subscription,
async def add_subscription_devices(db: AsyncSession, subscription: Subscription, devices: int) -> Subscription: async def add_subscription_devices(db: AsyncSession, subscription: Subscription, devices: int) -> Subscription:
subscription.device_limit += devices subscription.device_limit += devices
subscription.updated_at = datetime.utcnow() subscription.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(subscription) await db.refresh(subscription)
@@ -597,7 +597,7 @@ async def add_subscription_devices(db: AsyncSession, subscription: Subscription,
async def add_subscription_squad(db: AsyncSession, subscription: Subscription, squad_uuid: str) -> Subscription: async def add_subscription_squad(db: AsyncSession, subscription: Subscription, squad_uuid: str) -> Subscription:
if squad_uuid not in subscription.connected_squads: if squad_uuid not in subscription.connected_squads:
subscription.connected_squads = subscription.connected_squads + [squad_uuid] subscription.connected_squads = subscription.connected_squads + [squad_uuid]
subscription.updated_at = datetime.utcnow() subscription.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(subscription) await db.refresh(subscription)
@@ -612,7 +612,7 @@ async def remove_subscription_squad(db: AsyncSession, subscription: Subscription
squads = subscription.connected_squads.copy() squads = subscription.connected_squads.copy()
squads.remove(squad_uuid) squads.remove(squad_uuid)
subscription.connected_squads = squads subscription.connected_squads = squads
subscription.updated_at = datetime.utcnow() subscription.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(subscription) await db.refresh(subscription)
@@ -688,7 +688,7 @@ async def update_subscription_autopay(
) -> Subscription: ) -> Subscription:
subscription.autopay_enabled = enabled subscription.autopay_enabled = enabled
subscription.autopay_days_before = days_before subscription.autopay_days_before = days_before
subscription.updated_at = datetime.utcnow() subscription.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(subscription) await db.refresh(subscription)
@@ -700,7 +700,7 @@ async def update_subscription_autopay(
async def deactivate_subscription(db: AsyncSession, subscription: Subscription) -> Subscription: async def deactivate_subscription(db: AsyncSession, subscription: Subscription) -> Subscription:
subscription.status = SubscriptionStatus.DISABLED.value subscription.status = SubscriptionStatus.DISABLED.value
subscription.updated_at = datetime.utcnow() subscription.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(subscription) await db.refresh(subscription)
@@ -715,7 +715,7 @@ async def reactivate_subscription(db: AsyncSession, subscription: Subscription)
Активирует только если подписка была DISABLED и ещё не истекла. Активирует только если подписка была DISABLED и ещё не истекла.
Не логирует если реактивация не требуется. Не логирует если реактивация не требуется.
""" """
now = datetime.utcnow() now = datetime.now(UTC)
# Тихо выходим если реактивация не нужна # Тихо выходим если реактивация не нужна
if subscription.status != SubscriptionStatus.DISABLED.value: if subscription.status != SubscriptionStatus.DISABLED.value:
@@ -734,7 +734,7 @@ async def reactivate_subscription(db: AsyncSession, subscription: Subscription)
async def get_expiring_subscriptions(db: AsyncSession, days_before: int = 3) -> list[Subscription]: async def get_expiring_subscriptions(db: AsyncSession, days_before: int = 3) -> list[Subscription]:
threshold_date = datetime.utcnow() + timedelta(days=days_before) threshold_date = datetime.now(UTC) + timedelta(days=days_before)
result = await db.execute( result = await db.execute(
select(Subscription) select(Subscription)
@@ -743,7 +743,7 @@ async def get_expiring_subscriptions(db: AsyncSession, days_before: int = 3) ->
and_( and_(
Subscription.status == SubscriptionStatus.ACTIVE.value, Subscription.status == SubscriptionStatus.ACTIVE.value,
Subscription.end_date <= threshold_date, Subscription.end_date <= threshold_date,
Subscription.end_date > datetime.utcnow(), Subscription.end_date > datetime.now(UTC),
) )
) )
) )
@@ -754,13 +754,13 @@ async def get_expired_subscriptions(db: AsyncSession) -> list[Subscription]:
result = await db.execute( result = await db.execute(
select(Subscription) select(Subscription)
.options(selectinload(Subscription.user)) .options(selectinload(Subscription.user))
.where(and_(Subscription.status == SubscriptionStatus.ACTIVE.value, Subscription.end_date <= datetime.utcnow())) .where(and_(Subscription.status == SubscriptionStatus.ACTIVE.value, Subscription.end_date <= datetime.now(UTC)))
) )
return result.scalars().all() return result.scalars().all()
async def get_subscriptions_for_autopay(db: AsyncSession) -> list[Subscription]: async def get_subscriptions_for_autopay(db: AsyncSession) -> list[Subscription]:
current_time = datetime.utcnow() current_time = datetime.now(UTC)
result = await db.execute( result = await db.execute(
select(Subscription) select(Subscription)
@@ -811,7 +811,7 @@ async def get_subscriptions_statistics(db: AsyncSession) -> dict:
paid_subscriptions = active_subscriptions - trial_subscriptions paid_subscriptions = active_subscriptions - trial_subscriptions
today = datetime.utcnow().date() today = datetime.now(UTC).date()
today_result = await db.execute( today_result = await db.execute(
select(func.count(Subscription.id)).where( select(func.count(Subscription.id)).where(
and_(Subscription.created_at >= today, Subscription.is_trial == False) and_(Subscription.created_at >= today, Subscription.is_trial == False)
@@ -819,7 +819,7 @@ async def get_subscriptions_statistics(db: AsyncSession) -> dict:
) )
purchased_today = today_result.scalar() purchased_today = today_result.scalar()
week_ago = datetime.utcnow() - timedelta(days=7) week_ago = datetime.now(UTC) - timedelta(days=7)
week_result = await db.execute( week_result = await db.execute(
select(func.count(Subscription.id)).where( select(func.count(Subscription.id)).where(
and_(Subscription.created_at >= week_ago, Subscription.is_trial == False) and_(Subscription.created_at >= week_ago, Subscription.is_trial == False)
@@ -827,7 +827,7 @@ async def get_subscriptions_statistics(db: AsyncSession) -> dict:
) )
purchased_week = week_result.scalar() purchased_week = week_result.scalar()
month_ago = datetime.utcnow() - timedelta(days=30) month_ago = datetime.now(UTC) - timedelta(days=30)
month_result = await db.execute( month_result = await db.execute(
select(func.count(Subscription.id)).where( select(func.count(Subscription.id)).where(
and_(Subscription.created_at >= month_ago, Subscription.is_trial == False) and_(Subscription.created_at >= month_ago, Subscription.is_trial == False)
@@ -880,7 +880,7 @@ async def get_subscriptions_statistics(db: AsyncSession) -> dict:
async def get_trial_statistics(db: AsyncSession) -> dict: async def get_trial_statistics(db: AsyncSession) -> dict:
now = datetime.utcnow() now = datetime.now(UTC)
total_trials_result = await db.execute(select(func.count(Subscription.id)).where(Subscription.is_trial.is_(True))) total_trials_result = await db.execute(select(func.count(Subscription.id)).where(Subscription.is_trial.is_(True)))
total_trials = total_trials_result.scalar() or 0 total_trials = total_trials_result.scalar() or 0
@@ -913,7 +913,7 @@ async def get_trial_statistics(db: AsyncSession) -> dict:
async def reset_trials_for_users_without_paid_subscription(db: AsyncSession) -> int: async def reset_trials_for_users_without_paid_subscription(db: AsyncSession) -> int:
now = datetime.utcnow() now = datetime.now(UTC)
result = await db.execute( result = await db.execute(
select(Subscription) select(Subscription)
@@ -970,7 +970,7 @@ async def reset_trials_for_users_without_paid_subscription(db: AsyncSession) ->
async def update_subscription_usage(db: AsyncSession, subscription: Subscription, used_gb: float) -> Subscription: async def update_subscription_usage(db: AsyncSession, subscription: Subscription, used_gb: float) -> Subscription:
subscription.traffic_used_gb = used_gb subscription.traffic_used_gb = used_gb
subscription.updated_at = datetime.utcnow() subscription.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(subscription) await db.refresh(subscription)
@@ -1522,7 +1522,7 @@ async def calculate_addon_cost_for_remaining_period(
async def expire_subscription(db: AsyncSession, subscription: Subscription) -> Subscription: async def expire_subscription(db: AsyncSession, subscription: Subscription) -> Subscription:
subscription.status = SubscriptionStatus.EXPIRED.value subscription.status = SubscriptionStatus.EXPIRED.value
subscription.updated_at = datetime.utcnow() subscription.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(subscription) await db.refresh(subscription)
@@ -1532,7 +1532,7 @@ async def expire_subscription(db: AsyncSession, subscription: Subscription) -> S
async def check_and_update_subscription_status(db: AsyncSession, subscription: Subscription) -> Subscription: async def check_and_update_subscription_status(db: AsyncSession, subscription: Subscription) -> Subscription:
current_time = datetime.utcnow() current_time = datetime.now(UTC)
logger.info( logger.info(
'🔍 Проверка статуса подписки , текущий статус дата окончания текущее время', '🔍 Проверка статуса подписки , текущий статус дата окончания текущее время',
@@ -1595,7 +1595,7 @@ async def create_subscription_no_commit(
""" """
if end_date is None: if end_date is None:
end_date = datetime.utcnow() + timedelta(days=3) end_date = datetime.now(UTC) + timedelta(days=3)
if connected_squads is None: if connected_squads is None:
connected_squads = [] connected_squads = []
@@ -1645,7 +1645,7 @@ async def create_subscription(
autopay_days_before: int | None = None, autopay_days_before: int | None = None,
) -> Subscription: ) -> Subscription:
if end_date is None: if end_date is None:
end_date = datetime.utcnow() + timedelta(days=3) end_date = datetime.now(UTC) + timedelta(days=3)
if connected_squads is None: if connected_squads is None:
connected_squads = [] connected_squads = []
@@ -1693,7 +1693,7 @@ async def create_pending_subscription(
is_trial: If True, marks the subscription as a trial subscription. is_trial: If True, marks the subscription as a trial subscription.
""" """
trial_label = 'триальная ' if is_trial else '' trial_label = 'триальная ' if is_trial else ''
current_time = datetime.utcnow() current_time = datetime.now(UTC)
end_date = current_time + timedelta(days=duration_days) end_date = current_time + timedelta(days=duration_days)
existing_subscription = await get_subscription_by_user_id(db, user_id) existing_subscription = await get_subscription_by_user_id(db, user_id)
@@ -1809,7 +1809,7 @@ async def activate_pending_subscription(db: AsyncSession, user_id: int, period_d
) )
# Обновляем статус подписки на ACTIVE # Обновляем статус подписки на ACTIVE
current_time = datetime.utcnow() current_time = datetime.now(UTC)
pending_subscription.status = SubscriptionStatus.ACTIVE.value pending_subscription.status = SubscriptionStatus.ACTIVE.value
# Если указан период, обновляем дату окончания # Если указан период, обновляем дату окончания
@@ -1870,7 +1870,7 @@ async def activate_pending_trial_subscription(
) )
# Обновляем статус подписки на ACTIVE # Обновляем статус подписки на ACTIVE
current_time = datetime.utcnow() current_time = datetime.now(UTC)
pending_subscription.status = SubscriptionStatus.ACTIVE.value pending_subscription.status = SubscriptionStatus.ACTIVE.value
# Обновляем даты # Обновляем даты
@@ -1914,7 +1914,7 @@ async def get_daily_subscriptions_for_charge(db: AsyncSession) -> list[Subscript
""" """
from app.database.models import Tariff from app.database.models import Tariff
now = datetime.utcnow() now = datetime.now(UTC)
one_day_ago = now - timedelta(hours=24) one_day_ago = now - timedelta(hours=24)
query = ( query = (
@@ -2020,8 +2020,8 @@ async def resume_daily_subscription(
previous_status = subscription.status previous_status = subscription.status
subscription.status = SubscriptionStatus.ACTIVE.value subscription.status = SubscriptionStatus.ACTIVE.value
# Обновляем время последнего списания для корректного расчёта следующего # Обновляем время последнего списания для корректного расчёта следующего
subscription.last_daily_charge_at = datetime.utcnow() subscription.last_daily_charge_at = datetime.now(UTC)
subscription.end_date = datetime.utcnow() + timedelta(days=1) subscription.end_date = datetime.now(UTC) + timedelta(days=1)
logger.info( logger.info(
'✅ Суточная подписка восстановлена из в ACTIVE', '✅ Суточная подписка восстановлена из в ACTIVE',
subscription_id=subscription.id, subscription_id=subscription.id,
@@ -2044,7 +2044,7 @@ async def update_daily_charge_time(
charge_time: datetime = None, charge_time: datetime = None,
) -> Subscription: ) -> Subscription:
"""Обновляет время последнего суточного списания и продлевает подписку на 1 день.""" """Обновляет время последнего суточного списания и продлевает подписку на 1 день."""
now = charge_time or datetime.utcnow() now = charge_time or datetime.now(UTC)
subscription.last_daily_charge_at = now subscription.last_daily_charge_at = now
# Продлеваем подписку на 1 день от текущего момента # Продлеваем подписку на 1 день от текущего момента
+3 -3
View File
@@ -1,4 +1,4 @@
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
import structlog import structlog
from sqlalchemy import func, select from sqlalchemy import func, select
@@ -20,7 +20,7 @@ async def create_subscription_conversion(
) -> SubscriptionConversion: ) -> SubscriptionConversion:
conversion = SubscriptionConversion( conversion = SubscriptionConversion(
user_id=user_id, user_id=user_id,
converted_at=datetime.utcnow(), converted_at=datetime.now(UTC),
trial_duration_days=trial_duration_days, trial_duration_days=trial_duration_days,
payment_method=payment_method, payment_method=payment_method,
first_payment_amount_kopeks=first_payment_amount_kopeks, first_payment_amount_kopeks=first_payment_amount_kopeks,
@@ -82,7 +82,7 @@ async def get_conversion_statistics(db: AsyncSession) -> dict:
avg_first_payment_result = await db.execute(select(func.avg(SubscriptionConversion.first_payment_amount_kopeks))) avg_first_payment_result = await db.execute(select(func.avg(SubscriptionConversion.first_payment_amount_kopeks)))
avg_first_payment = avg_first_payment_result.scalar() or 0 avg_first_payment = avg_first_payment_result.scalar() or 0
month_ago = datetime.utcnow() - timedelta(days=30) month_ago = datetime.now(UTC) - timedelta(days=30)
month_conversions_result = await db.execute( month_conversions_result = await db.execute(
select(func.count(SubscriptionConversion.id)).where(SubscriptionConversion.converted_at >= month_ago) select(func.count(SubscriptionConversion.id)).where(SubscriptionConversion.converted_at >= month_ago)
) )
+2 -2
View File
@@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Iterable from collections.abc import Iterable
from datetime import datetime from datetime import UTC, datetime
from typing import Any from typing import Any
from sqlalchemy import and_, func, select from sqlalchemy import and_, func, select
@@ -32,7 +32,7 @@ async def create_subscription_event(
amount_kopeks=amount_kopeks, amount_kopeks=amount_kopeks,
currency=currency, currency=currency,
message=message, message=message,
occurred_at=occurred_at or datetime.utcnow(), occurred_at=occurred_at or datetime.now(UTC),
extra=extra or None, extra=extra or None,
) )
db.add(event) db.add(event)
+9 -9
View File
@@ -1,4 +1,4 @@
from datetime import datetime from datetime import UTC, datetime
import structlog import structlog
from sqlalchemy import and_, desc, func, or_, select, update from sqlalchemy import and_, desc, func, or_, select, update
@@ -154,18 +154,18 @@ class TicketCRUD:
tickets = result.scalars().all() tickets = result.scalars().all()
if not tickets: if not tickets:
return None return None
from datetime import datetime from datetime import UTC, datetime
# Если есть вечная блокировка в любом тикете — блок активен без срока # Если есть вечная блокировка в любом тикете — блок активен без срока
for t in tickets: for t in tickets:
if t.user_reply_block_permanent: if t.user_reply_block_permanent:
return datetime.max return datetime.max.replace(tzinfo=UTC)
# Иначе ищем максимальный срок блокировки, если он в будущем # Иначе ищем максимальный срок блокировки, если он в будущем
future_until = [t.user_reply_block_until for t in tickets if t.user_reply_block_until] future_until = [t.user_reply_block_until for t in tickets if t.user_reply_block_until]
if not future_until: if not future_until:
return None return None
max_until = max(future_until) max_until = max(future_until)
return max_until if max_until > datetime.utcnow() else None return max_until if max_until > datetime.now(UTC) else None
@staticmethod @staticmethod
async def get_all_tickets( async def get_all_tickets(
@@ -225,7 +225,7 @@ class TicketCRUD:
return False return False
ticket.status = status ticket.status = status
ticket.updated_at = datetime.utcnow() ticket.updated_at = datetime.now(UTC)
if status == TicketStatus.CLOSED.value and closed_at: if status == TicketStatus.CLOSED.value and closed_at:
ticket.closed_at = closed_at ticket.closed_at = closed_at
@@ -259,14 +259,14 @@ class TicketCRUD:
return False return False
ticket.user_reply_block_permanent = bool(permanent) ticket.user_reply_block_permanent = bool(permanent)
ticket.user_reply_block_until = until ticket.user_reply_block_until = until
ticket.updated_at = datetime.utcnow() ticket.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
return True return True
@staticmethod @staticmethod
async def close_ticket(db: AsyncSession, ticket_id: int) -> bool: async def close_ticket(db: AsyncSession, ticket_id: int) -> bool:
"""Закрыть тикет""" """Закрыть тикет"""
return await TicketCRUD.update_ticket_status(db, ticket_id, TicketStatus.CLOSED.value, datetime.utcnow()) return await TicketCRUD.update_ticket_status(db, ticket_id, TicketStatus.CLOSED.value, datetime.now(UTC))
@staticmethod @staticmethod
async def close_all_open_tickets( async def close_all_open_tickets(
@@ -280,7 +280,7 @@ class TicketCRUD:
if not ticket_ids: if not ticket_ids:
return [] return []
now = datetime.utcnow() now = datetime.now(UTC)
await db.execute( await db.execute(
update(Ticket) update(Ticket)
.where(Ticket.id.in_(ticket_ids)) .where(Ticket.id.in_(ticket_ids))
@@ -417,7 +417,7 @@ class TicketMessageCRUD:
except Exception: except Exception:
pass pass
ticket.updated_at = datetime.utcnow() ticket.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(message) await db.refresh(message)
+6 -6
View File
@@ -1,6 +1,6 @@
"""CRUD operations for TicketNotification.""" """CRUD operations for TicketNotification."""
from datetime import datetime from datetime import UTC, datetime
import structlog import structlog
from sqlalchemy import desc, func, select, update from sqlalchemy import desc, func, select, update
@@ -40,7 +40,7 @@ class TicketNotificationCRUD:
message=message, message=message,
is_for_admin=is_for_admin, is_for_admin=is_for_admin,
is_read=False, is_read=False,
created_at=datetime.utcnow(), created_at=datetime.now(UTC),
) )
db.add(notification) db.add(notification)
await db.commit() await db.commit()
@@ -130,7 +130,7 @@ class TicketNotificationCRUD:
query = ( query = (
update(TicketNotification) update(TicketNotification)
.where(TicketNotification.id == notification_id) .where(TicketNotification.id == notification_id)
.values(is_read=True, read_at=datetime.utcnow()) .values(is_read=True, read_at=datetime.now(UTC))
) )
result = await db.execute(query) result = await db.execute(query)
await db.commit() await db.commit()
@@ -146,7 +146,7 @@ class TicketNotificationCRUD:
TicketNotification.is_for_admin == False, TicketNotification.is_for_admin == False,
TicketNotification.is_read == False, TicketNotification.is_read == False,
) )
.values(is_read=True, read_at=datetime.utcnow()) .values(is_read=True, read_at=datetime.now(UTC))
) )
result = await db.execute(query) result = await db.execute(query)
await db.commit() await db.commit()
@@ -161,7 +161,7 @@ class TicketNotificationCRUD:
TicketNotification.is_for_admin == True, TicketNotification.is_for_admin == True,
TicketNotification.is_read == False, TicketNotification.is_read == False,
) )
.values(is_read=True, read_at=datetime.utcnow()) .values(is_read=True, read_at=datetime.now(UTC))
) )
result = await db.execute(query) result = await db.execute(query)
await db.commit() await db.commit()
@@ -178,7 +178,7 @@ class TicketNotificationCRUD:
TicketNotification.ticket_id == ticket_id, TicketNotification.ticket_id == ticket_id,
TicketNotification.is_read == False, TicketNotification.is_read == False,
) )
.values(is_read=True, read_at=datetime.utcnow()) .values(is_read=True, read_at=datetime.now(UTC))
) )
if is_admin: if is_admin:
+9 -9
View File
@@ -1,4 +1,4 @@
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
import structlog import structlog
from sqlalchemy import and_, func, or_, select from sqlalchemy import and_, func, or_, select
@@ -47,7 +47,7 @@ async def create_transaction(
payment_method=payment_method.value if payment_method else None, payment_method=payment_method.value if payment_method else None,
external_id=external_id, external_id=external_id,
is_completed=is_completed, is_completed=is_completed,
completed_at=datetime.utcnow() if is_completed else None, completed_at=datetime.now(UTC) if is_completed else None,
**({'created_at': created_at} if created_at else {}), **({'created_at': created_at} if created_at else {}),
) )
@@ -163,7 +163,7 @@ async def get_user_total_spent_kopeks(db: AsyncSession, user_id: int) -> int:
async def complete_transaction(db: AsyncSession, transaction: Transaction) -> Transaction: async def complete_transaction(db: AsyncSession, transaction: Transaction) -> Transaction:
transaction.is_completed = True transaction.is_completed = True
transaction.completed_at = datetime.utcnow() transaction.completed_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(transaction) await db.refresh(transaction)
@@ -198,9 +198,9 @@ async def get_transactions_statistics(
db: AsyncSession, start_date: datetime | None = None, end_date: datetime | None = None db: AsyncSession, start_date: datetime | None = None, end_date: datetime | None = None
) -> dict: ) -> dict:
if not start_date: if not start_date:
start_date = datetime.utcnow().replace(day=1, hour=0, minute=0, second=0, microsecond=0) start_date = datetime.now(UTC).replace(day=1, hour=0, minute=0, second=0, microsecond=0)
if not end_date: if not end_date:
end_date = datetime.utcnow() end_date = datetime.now(UTC)
# Доход считаем только по реальным платежам (исключаем колесо, промокоды, админские пополнения) # Доход считаем только по реальным платежам (исключаем колесо, промокоды, админские пополнения)
income_result = await db.execute( income_result = await db.execute(
@@ -279,7 +279,7 @@ async def get_transactions_statistics(
row.payment_method: {'count': row.count, 'amount': row.total_amount} for row in payment_methods_result row.payment_method: {'count': row.count, 'amount': row.total_amount} for row in payment_methods_result
} }
today = datetime.utcnow().date() today = datetime.now(UTC).date()
today_result = await db.execute( today_result = await db.execute(
select(func.count(Transaction.id)).where( select(func.count(Transaction.id)).where(
and_(Transaction.is_completed == True, Transaction.created_at >= today) and_(Transaction.is_completed == True, Transaction.created_at >= today)
@@ -316,7 +316,7 @@ async def get_transactions_statistics(
async def get_revenue_by_period(db: AsyncSession, days: int = 30) -> list[dict]: async def get_revenue_by_period(db: AsyncSession, days: int = 30) -> list[dict]:
"""Доход по дням - только реальные платежи.""" """Доход по дням - только реальные платежи."""
start_date = datetime.utcnow() - timedelta(days=days) start_date = datetime.now(UTC) - timedelta(days=days)
result = await db.execute( result = await db.execute(
select( select(
@@ -363,7 +363,7 @@ async def find_tribute_transactions_by_payment_id(
async def check_tribute_payment_duplicate( async def check_tribute_payment_duplicate(
db: AsyncSession, payment_id: str, amount_kopeks: int, user_telegram_id: int db: AsyncSession, payment_id: str, amount_kopeks: int, user_telegram_id: int
) -> Transaction | None: ) -> Transaction | None:
cutoff_time = datetime.utcnow() - timedelta(hours=24) cutoff_time = datetime.now(UTC) - timedelta(hours=24)
exact_external_id = f'donation_{payment_id}' exact_external_id = f'donation_{payment_id}'
@@ -400,7 +400,7 @@ async def create_unique_tribute_transaction(
existing = await get_transaction_by_external_id(db, external_id, PaymentMethod.TRIBUTE) existing = await get_transaction_by_external_id(db, external_id, PaymentMethod.TRIBUTE)
if existing: if existing:
timestamp = int(datetime.utcnow().timestamp()) timestamp = int(datetime.now(UTC).timestamp())
external_id = f'donation_{payment_id}_{amount_kopeks}_{timestamp}' external_id = f'donation_{payment_id}_{amount_kopeks}_{timestamp}'
logger.info('Создан уникальный external_id для избежания дубликатов', external_id=external_id) logger.info('Создан уникальный external_id для избежания дубликатов', external_id=external_id)
+18 -18
View File
@@ -195,7 +195,7 @@ async def create_unique_referral_code(db: AsyncSession) -> str:
if not existing_user: if not existing_user:
return code return code
timestamp = str(int(datetime.utcnow().timestamp()))[-6:] timestamp = str(int(datetime.now(UTC).timestamp()))[-6:]
return f'ref{timestamp}' return f'ref{timestamp}'
@@ -379,7 +379,7 @@ async def update_user(db: AsyncSession, user: User, **kwargs) -> User:
if hasattr(user, field): if hasattr(user, field):
setattr(user, field, value) setattr(user, field, value)
user.updated_at = datetime.utcnow() user.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(user) await db.refresh(user)
@@ -399,7 +399,7 @@ async def add_user_balance(
try: try:
old_balance = user.balance_kopeks old_balance = user.balance_kopeks
user.balance_kopeks += amount_kopeks user.balance_kopeks += amount_kopeks
user.updated_at = datetime.utcnow() user.updated_at = datetime.now(UTC)
if create_transaction: if create_transaction:
from app.database.crud.transaction import create_transaction as create_trans from app.database.crud.transaction import create_transaction as create_trans
@@ -564,7 +564,7 @@ async def subtract_user_balance(
user.promo_offer_discount_source = None user.promo_offer_discount_source = None
user.promo_offer_discount_expires_at = None user.promo_offer_discount_expires_at = None
user.updated_at = datetime.utcnow() user.updated_at = datetime.now(UTC)
if create_transaction: if create_transaction:
from app.database.crud.transaction import ( from app.database.crud.transaction import (
@@ -620,7 +620,7 @@ async def subtract_user_balance(
async def cleanup_expired_promo_offer_discounts(db: AsyncSession) -> int: async def cleanup_expired_promo_offer_discounts(db: AsyncSession) -> int:
now = datetime.utcnow() now = datetime.now(UTC)
result = await db.execute( result = await db.execute(
select(User).where( select(User).where(
User.promo_offer_discount_percent > 0, User.promo_offer_discount_percent > 0,
@@ -899,7 +899,7 @@ async def get_referrals(db: AsyncSession, user_id: int) -> list[User]:
async def get_users_for_promo_segment(db: AsyncSession, segment: str) -> list[User]: async def get_users_for_promo_segment(db: AsyncSession, segment: str) -> list[User]:
now = datetime.utcnow() now = datetime.now(UTC)
base_query = ( base_query = (
select(User) select(User)
@@ -961,7 +961,7 @@ async def get_users_for_promo_segment(db: AsyncSession, segment: str) -> list[Us
async def get_inactive_users(db: AsyncSession, months: int = 3) -> list[User]: async def get_inactive_users(db: AsyncSession, months: int = 3) -> list[User]:
threshold_date = datetime.utcnow() - timedelta(days=months * 30) threshold_date = datetime.now(UTC) - timedelta(days=months * 30)
result = await db.execute( result = await db.execute(
select(User) select(User)
@@ -986,7 +986,7 @@ async def get_inactive_users(db: AsyncSession, months: int = 3) -> list[User]:
async def delete_user(db: AsyncSession, user: User) -> bool: async def delete_user(db: AsyncSession, user: User) -> bool:
user.status = UserStatus.DELETED.value user.status = UserStatus.DELETED.value
user.updated_at = datetime.utcnow() user.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
user_id_display = user.telegram_id or user.email or f'#{user.id}' user_id_display = user.telegram_id or user.email or f'#{user.id}'
@@ -1001,19 +1001,19 @@ async def get_users_statistics(db: AsyncSession) -> dict:
active_result = await db.execute(select(func.count(User.id)).where(User.status == UserStatus.ACTIVE.value)) active_result = await db.execute(select(func.count(User.id)).where(User.status == UserStatus.ACTIVE.value))
active_users = active_result.scalar() active_users = active_result.scalar()
today = datetime.utcnow().date() today = datetime.now(UTC).date()
today_result = await db.execute( today_result = await db.execute(
select(func.count(User.id)).where(and_(User.created_at >= today, User.status == UserStatus.ACTIVE.value)) select(func.count(User.id)).where(and_(User.created_at >= today, User.status == UserStatus.ACTIVE.value))
) )
new_today = today_result.scalar() new_today = today_result.scalar()
week_ago = datetime.utcnow() - timedelta(days=7) week_ago = datetime.now(UTC) - timedelta(days=7)
week_result = await db.execute( week_result = await db.execute(
select(func.count(User.id)).where(and_(User.created_at >= week_ago, User.status == UserStatus.ACTIVE.value)) select(func.count(User.id)).where(and_(User.created_at >= week_ago, User.status == UserStatus.ACTIVE.value))
) )
new_week = week_result.scalar() new_week = week_result.scalar()
month_ago = datetime.utcnow() - timedelta(days=30) month_ago = datetime.now(UTC) - timedelta(days=30)
month_result = await db.execute( month_result = await db.execute(
select(func.count(User.id)).where(and_(User.created_at >= month_ago, User.status == UserStatus.ACTIVE.value)) select(func.count(User.id)).where(and_(User.created_at >= month_ago, User.status == UserStatus.ACTIVE.value))
) )
@@ -1037,7 +1037,7 @@ async def get_users_with_active_subscriptions(db: AsyncSession) -> list[User]:
Returns: Returns:
Список пользователей с активными подписками и remnawave_uuid Список пользователей с активными подписками и remnawave_uuid
""" """
current_time = datetime.utcnow() current_time = datetime.now(UTC)
result = await db.execute( result = await db.execute(
select(User) select(User)
@@ -1177,7 +1177,7 @@ async def set_email_change_pending(
user.email_change_new = new_email user.email_change_new = new_email
user.email_change_code = code user.email_change_code = code
user.email_change_expires = expires_at user.email_change_expires = expires_at
user.updated_at = datetime.utcnow() user.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(user) await db.refresh(user)
@@ -1201,7 +1201,7 @@ async def verify_and_apply_email_change(db: AsyncSession, user: User, code: str)
if not user.email_change_new or not user.email_change_code: if not user.email_change_new or not user.email_change_code:
return False, 'No pending email change' return False, 'No pending email change'
if user.email_change_expires and datetime.utcnow() > user.email_change_expires: if user.email_change_expires and datetime.now(UTC) > user.email_change_expires:
# Clear expired data # Clear expired data
user.email_change_new = None user.email_change_new = None
user.email_change_code = None user.email_change_code = None
@@ -1227,11 +1227,11 @@ async def verify_and_apply_email_change(db: AsyncSession, user: User, code: str)
# Apply the change # Apply the change
user.email = new_email user.email = new_email
user.email_verified = True user.email_verified = True
user.email_verified_at = datetime.utcnow() user.email_verified_at = datetime.now(UTC)
user.email_change_new = None user.email_change_new = None
user.email_change_code = None user.email_change_code = None
user.email_change_expires = None user.email_change_expires = None
user.updated_at = datetime.utcnow() user.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(user) await db.refresh(user)
@@ -1251,7 +1251,7 @@ async def clear_email_change_pending(db: AsyncSession, user: User) -> None:
user.email_change_new = None user.email_change_new = None
user.email_change_code = None user.email_change_code = None
user.email_change_expires = None user.email_change_expires = None
user.updated_at = datetime.utcnow() user.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
logger.info('Email change cancelled for user', user_id=user.id) logger.info('Email change cancelled for user', user_id=user.id)
@@ -1286,7 +1286,7 @@ async def set_user_oauth_provider_id(db: AsyncSession, user: User, provider: str
return return
value: str | int = int(provider_id) if provider == 'vk' else provider_id value: str | int = int(provider_id) if provider == 'vk' else provider_id
setattr(user, column_name, value) setattr(user, column_name, value)
user.updated_at = datetime.now(UTC).replace(tzinfo=None) user.updated_at = datetime.now(UTC)
logger.info('Linked (id=) to user', provider=provider, provider_id=provider_id, user_id=user.id) logger.info('Linked (id=) to user', provider=provider, provider_id=provider_id, user_id=user.id)
+3 -3
View File
@@ -1,5 +1,5 @@
import random import random
from datetime import datetime from datetime import UTC, datetime
import structlog import structlog
from sqlalchemy import func, select from sqlalchemy import func, select
@@ -111,7 +111,7 @@ async def update_user_message(
if sort_order is not None: if sort_order is not None:
message.sort_order = sort_order message.sort_order = sort_order
message.updated_at = datetime.utcnow() message.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(message) await db.refresh(message)
@@ -127,7 +127,7 @@ async def toggle_user_message_status(db: AsyncSession, message_id: int) -> UserM
return None return None
message.is_active = not message.is_active message.is_active = not message.is_active
message.updated_at = datetime.utcnow() message.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(message) await db.refresh(message)
+2 -2
View File
@@ -1,6 +1,6 @@
"""CRUD операции для связи пользователей с промогруппами (Many-to-Many).""" """CRUD операции для связи пользователей с промогруппами (Many-to-Many)."""
from datetime import datetime from datetime import UTC, datetime
import structlog import structlog
from sqlalchemy import and_, desc, select from sqlalchemy import and_, desc, select
@@ -36,7 +36,7 @@ async def _sync_user_primary_promo_group(
if user.promo_group_id != new_primary_id: if user.promo_group_id != new_primary_id:
user.promo_group_id = new_primary_id user.promo_group_id = new_primary_id
user.updated_at = datetime.utcnow() user.updated_at = datetime.now(UTC)
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)
+3 -3
View File
@@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Iterable from collections.abc import Iterable
from datetime import datetime from datetime import UTC, datetime
from sqlalchemy import select, update from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -71,7 +71,7 @@ async def update_token(
for key, value in kwargs.items(): for key, value in kwargs.items():
if hasattr(token, key): if hasattr(token, key):
setattr(token, key, value) setattr(token, key, value)
token.updated_at = datetime.utcnow() token.updated_at = datetime.now(UTC)
await db.flush() await db.flush()
await db.refresh(token) await db.refresh(token)
return token return token
@@ -86,7 +86,7 @@ async def set_tokens_active_status(
await db.execute( await db.execute(
update(WebApiToken) update(WebApiToken)
.where(WebApiToken.id.in_(list(token_ids))) .where(WebApiToken.id.in_(list(token_ids)))
.values(is_active=is_active, updated_at=datetime.utcnow()) .values(is_active=is_active, updated_at=datetime.now(UTC))
) )
+4 -4
View File
@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime from datetime import UTC, datetime
from sqlalchemy import func, select from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -94,7 +94,7 @@ async def update_webhook(
if is_active is not None: if is_active is not None:
webhook.is_active = is_active webhook.is_active = is_active
webhook.updated_at = datetime.utcnow() webhook.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(webhook) await db.refresh(webhook)
return webhook return webhook
@@ -127,7 +127,7 @@ async def record_webhook_delivery(
response_body=response_body, response_body=response_body,
error_message=error_message, error_message=error_message,
attempt_number=attempt_number, attempt_number=attempt_number,
delivered_at=datetime.utcnow() if status == 'success' else None, delivered_at=datetime.now(UTC) if status == 'success' else None,
) )
db.add(delivery) db.add(delivery)
await db.commit() await db.commit()
@@ -145,7 +145,7 @@ async def update_webhook_stats(
webhook.success_count += 1 webhook.success_count += 1
else: else:
webhook.failure_count += 1 webhook.failure_count += 1
webhook.last_triggered_at = datetime.utcnow() webhook.last_triggered_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(webhook) await db.refresh(webhook)
return webhook return webhook
+3 -3
View File
@@ -1,4 +1,4 @@
from datetime import datetime from datetime import UTC, datetime
import structlog import structlog
from sqlalchemy import func, select, update from sqlalchemy import func, select, update
@@ -77,7 +77,7 @@ async def toggle_welcome_text_status(db: AsyncSession, admin_id: int) -> bool:
if welcome_text: if welcome_text:
welcome_text.is_enabled = not welcome_text.is_enabled welcome_text.is_enabled = not welcome_text.is_enabled
welcome_text.updated_at = datetime.utcnow() welcome_text.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(welcome_text) await db.refresh(welcome_text)
@@ -182,7 +182,7 @@ async def update_welcome_text(
if is_active is not None: if is_active is not None:
welcome_text.is_active = is_active welcome_text.is_active = is_active
welcome_text.updated_at = datetime.utcnow() welcome_text.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(welcome_text) await db.refresh(welcome_text)
+6 -6
View File
@@ -2,7 +2,7 @@
CRUD операции для колеса удачи (Fortune Wheel). CRUD операции для колеса удачи (Fortune Wheel).
""" """
from datetime import datetime from datetime import UTC, datetime
from typing import Any from typing import Any
import structlog import structlog
@@ -65,7 +65,7 @@ async def update_wheel_config(db: AsyncSession, **kwargs) -> WheelConfig:
if hasattr(config, key) and value is not None: if hasattr(config, key) and value is not None:
setattr(config, key, value) setattr(config, key, value)
config.updated_at = datetime.utcnow() config.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(config) await db.refresh(config)
logger.info('🎡 Обновлена конфигурация колеса', kwargs=kwargs) logger.info('🎡 Обновлена конфигурация колеса', kwargs=kwargs)
@@ -143,7 +143,7 @@ async def update_wheel_prize(db: AsyncSession, prize_id: int, **kwargs) -> Wheel
if hasattr(prize, key) and value is not None: if hasattr(prize, key) and value is not None:
setattr(prize, key, value) setattr(prize, key, value)
prize.updated_at = datetime.utcnow() prize.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(prize) await db.refresh(prize)
logger.info('🎁 Обновлен приз колеса ID', prize_id=prize_id, kwargs=kwargs) logger.info('🎁 Обновлен приз колеса ID', prize_id=prize_id, kwargs=kwargs)
@@ -204,7 +204,7 @@ async def create_wheel_spin(
prize_value_kopeks=prize_value_kopeks, prize_value_kopeks=prize_value_kopeks,
generated_promocode_id=generated_promocode_id, generated_promocode_id=generated_promocode_id,
is_applied=is_applied, is_applied=is_applied,
applied_at=datetime.utcnow() if is_applied else None, applied_at=datetime.now(UTC) if is_applied else None,
) )
db.add(spin) db.add(spin)
await db.commit() await db.commit()
@@ -219,7 +219,7 @@ async def mark_spin_applied(db: AsyncSession, spin_id: int) -> WheelSpin | None:
spin = result.scalar_one_or_none() spin = result.scalar_one_or_none()
if spin: if spin:
spin.is_applied = True spin.is_applied = True
spin.applied_at = datetime.utcnow() spin.applied_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(spin) await db.refresh(spin)
return spin return spin
@@ -227,7 +227,7 @@ async def mark_spin_applied(db: AsyncSession, spin_id: int) -> WheelSpin | None:
async def get_user_spins_today(db: AsyncSession, user_id: int) -> int: async def get_user_spins_today(db: AsyncSession, user_id: int) -> int:
"""Получить количество спинов пользователя за сегодня.""" """Получить количество спинов пользователя за сегодня."""
today_start = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) today_start = datetime.now(UTC).replace(hour=0, minute=0, second=0, microsecond=0)
result = await db.execute( result = await db.execute(
select(func.count(WheelSpin.id)).where( select(func.count(WheelSpin.id)).where(
+3 -3
View File
@@ -1,4 +1,4 @@
from datetime import datetime from datetime import UTC, datetime
import structlog import structlog
from sqlalchemy import and_, select, update from sqlalchemy import and_, select, update
@@ -88,7 +88,7 @@ async def update_yookassa_payment_status(
captured_at: datetime | None = None, captured_at: datetime | None = None,
payment_method_type: str | None = None, payment_method_type: str | None = None,
) -> YooKassaPayment | None: ) -> YooKassaPayment | None:
update_data = {'status': status, 'is_paid': is_paid, 'is_captured': is_captured, 'updated_at': datetime.utcnow()} update_data = {'status': status, 'is_paid': is_paid, 'is_captured': is_captured, 'updated_at': datetime.now(UTC)}
if captured_at: if captured_at:
update_data['captured_at'] = captured_at update_data['captured_at'] = captured_at
@@ -125,7 +125,7 @@ async def link_yookassa_payment_to_transaction(
await db.execute( await db.execute(
update(YooKassaPayment) update(YooKassaPayment)
.where(YooKassaPayment.yookassa_payment_id == yookassa_payment_id) .where(YooKassaPayment.yookassa_payment_id == yookassa_payment_id)
.values(transaction_id=transaction_id, updated_at=datetime.utcnow()) .values(transaction_id=transaction_id, updated_at=datetime.now(UTC))
) )
await db.commit() await db.commit()
+184 -185
View File
@@ -1,4 +1,4 @@
from datetime import datetime, time, timedelta from datetime import UTC, datetime, time, timedelta
from enum import Enum from enum import Enum
from sqlalchemy import ( from sqlalchemy import (
@@ -176,10 +176,10 @@ class YooKassaPayment(Base):
payment_method_type = Column(String(50), nullable=True) payment_method_type = Column(String(50), nullable=True)
refundable = Column(Boolean, default=False) refundable = Column(Boolean, default=False)
test_mode = Column(Boolean, default=False) test_mode = Column(Boolean, default=False)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
yookassa_created_at = Column(DateTime, nullable=True) yookassa_created_at = Column(DateTime(timezone=True), nullable=True)
captured_at = Column(DateTime, nullable=True) captured_at = Column(DateTime(timezone=True), nullable=True)
user = relationship('User', backref='yookassa_payments') user = relationship('User', backref='yookassa_payments')
transaction = relationship('Transaction', backref='yookassa_payment') transaction = relationship('Transaction', backref='yookassa_payment')
@@ -225,11 +225,11 @@ class CryptoBotPayment(Base):
mini_app_invoice_url = Column(Text, nullable=True) mini_app_invoice_url = Column(Text, nullable=True)
web_app_invoice_url = Column(Text, nullable=True) web_app_invoice_url = Column(Text, nullable=True)
paid_at = Column(DateTime, nullable=True) paid_at = Column(DateTime(timezone=True), nullable=True)
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True) transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
user = relationship('User', backref='cryptobot_payments') user = relationship('User', backref='cryptobot_payments')
transaction = relationship('Transaction', backref='cryptobot_payment') transaction = relationship('Transaction', backref='cryptobot_payment')
@@ -277,12 +277,12 @@ class HeleketPayment(Base):
payment_url = Column(Text, nullable=True) payment_url = Column(Text, nullable=True)
metadata_json = Column(JSON, nullable=True) metadata_json = Column(JSON, nullable=True)
paid_at = Column(DateTime, nullable=True) paid_at = Column(DateTime(timezone=True), nullable=True)
expires_at = Column(DateTime, nullable=True) expires_at = Column(DateTime(timezone=True), nullable=True)
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True) transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
user = relationship('User', backref='heleket_payments') user = relationship('User', backref='heleket_payments')
transaction = relationship('Transaction', backref='heleket_payment') transaction = relationship('Transaction', backref='heleket_payment')
@@ -330,7 +330,7 @@ class MulenPayPayment(Base):
status = Column(String(50), nullable=False, default='created') status = Column(String(50), nullable=False, default='created')
is_paid = Column(Boolean, default=False) is_paid = Column(Boolean, default=False)
paid_at = Column(DateTime, nullable=True) paid_at = Column(DateTime(timezone=True), nullable=True)
payment_url = Column(Text, nullable=True) payment_url = Column(Text, nullable=True)
metadata_json = Column(JSON, nullable=True) metadata_json = Column(JSON, nullable=True)
@@ -338,8 +338,8 @@ class MulenPayPayment(Base):
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True) transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
user = relationship('User', backref='mulenpay_payments') user = relationship('User', backref='mulenpay_payments')
transaction = relationship('Transaction', backref='mulenpay_payment') transaction = relationship('Transaction', backref='mulenpay_payment')
@@ -368,9 +368,9 @@ class Pal24Payment(Base):
status = Column(String(50), nullable=False, default='NEW') status = Column(String(50), nullable=False, default='NEW')
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
is_paid = Column(Boolean, default=False) is_paid = Column(Boolean, default=False)
paid_at = Column(DateTime, nullable=True) paid_at = Column(DateTime(timezone=True), nullable=True)
last_status = Column(String(50), nullable=True) last_status = Column(String(50), nullable=True)
last_status_checked_at = Column(DateTime, nullable=True) last_status_checked_at = Column(DateTime(timezone=True), nullable=True)
link_url = Column(Text, nullable=True) link_url = Column(Text, nullable=True)
link_page_url = Column(Text, nullable=True) link_page_url = Column(Text, nullable=True)
@@ -385,12 +385,12 @@ class Pal24Payment(Base):
payer_account = Column(String(255), nullable=True) payer_account = Column(String(255), nullable=True)
ttl = Column(Integer, nullable=True) ttl = Column(Integer, nullable=True)
expires_at = Column(DateTime, nullable=True) expires_at = Column(DateTime(timezone=True), nullable=True)
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True) transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
user = relationship('User', backref='pal24_payments') user = relationship('User', backref='pal24_payments')
transaction = relationship('Transaction', backref='pal24_payment') transaction = relationship('Transaction', backref='pal24_payment')
@@ -424,7 +424,7 @@ class WataPayment(Base):
status = Column(String(50), nullable=False, default='Opened') status = Column(String(50), nullable=False, default='Opened')
is_paid = Column(Boolean, default=False) is_paid = Column(Boolean, default=False)
paid_at = Column(DateTime, nullable=True) paid_at = Column(DateTime(timezone=True), nullable=True)
last_status = Column(String(50), nullable=True) last_status = Column(String(50), nullable=True)
terminal_public_id = Column(String(64), nullable=True) terminal_public_id = Column(String(64), nullable=True)
@@ -434,12 +434,12 @@ class WataPayment(Base):
metadata_json = Column(JSON, nullable=True) metadata_json = Column(JSON, nullable=True)
callback_payload = Column(JSON, nullable=True) callback_payload = Column(JSON, nullable=True)
expires_at = Column(DateTime, nullable=True) expires_at = Column(DateTime(timezone=True), nullable=True)
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True) transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
user = relationship('User', backref='wata_payments') user = relationship('User', backref='wata_payments')
transaction = relationship('Transaction', backref='wata_payment') transaction = relationship('Transaction', backref='wata_payment')
@@ -467,7 +467,7 @@ class PlategaPayment(Base):
payment_method_code = Column(Integer, nullable=False) payment_method_code = Column(Integer, nullable=False)
status = Column(String(50), nullable=False, default='PENDING') status = Column(String(50), nullable=False, default='PENDING')
is_paid = Column(Boolean, default=False) is_paid = Column(Boolean, default=False)
paid_at = Column(DateTime, nullable=True) paid_at = Column(DateTime(timezone=True), nullable=True)
redirect_url = Column(Text, nullable=True) redirect_url = Column(Text, nullable=True)
return_url = Column(Text, nullable=True) return_url = Column(Text, nullable=True)
@@ -476,12 +476,12 @@ class PlategaPayment(Base):
metadata_json = Column(JSON, nullable=True) metadata_json = Column(JSON, nullable=True)
callback_payload = Column(JSON, nullable=True) callback_payload = Column(JSON, nullable=True)
expires_at = Column(DateTime, nullable=True) expires_at = Column(DateTime(timezone=True), nullable=True)
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True) transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
user = relationship('User', backref='platega_payments') user = relationship('User', backref='platega_payments')
transaction = relationship('Transaction', backref='platega_payment') transaction = relationship('Transaction', backref='platega_payment')
@@ -510,7 +510,7 @@ class CloudPaymentsPayment(Base):
status = Column(String(50), nullable=False, default='pending') # pending, completed, failed, authorized status = Column(String(50), nullable=False, default='pending') # pending, completed, failed, authorized
is_paid = Column(Boolean, default=False) is_paid = Column(Boolean, default=False)
paid_at = Column(DateTime, nullable=True) paid_at = Column(DateTime(timezone=True), nullable=True)
# Данные карты (маскированные) # Данные карты (маскированные)
card_first_six = Column(String(6), nullable=True) card_first_six = Column(String(6), nullable=True)
@@ -537,8 +537,8 @@ class CloudPaymentsPayment(Base):
# Связь с транзакцией в нашей системе # Связь с транзакцией в нашей системе
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True) transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
user = relationship('User', backref='cloudpayments_payments') user = relationship('User', backref='cloudpayments_payments')
transaction = relationship('Transaction', backref='cloudpayments_payment') transaction = relationship('Transaction', backref='cloudpayments_payment')
@@ -591,10 +591,10 @@ class FreekassaPayment(Base):
callback_payload = Column(JSON, nullable=True) callback_payload = Column(JSON, nullable=True)
# Временные метки # Временные метки
paid_at = Column(DateTime, nullable=True) paid_at = Column(DateTime(timezone=True), nullable=True)
expires_at = Column(DateTime, nullable=True) expires_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
# Связь с транзакцией # Связь с транзакцией
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True) transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
@@ -653,10 +653,10 @@ class KassaAiPayment(Base):
callback_payload = Column(JSON, nullable=True) callback_payload = Column(JSON, nullable=True)
# Временные метки # Временные метки
paid_at = Column(DateTime, nullable=True) paid_at = Column(DateTime(timezone=True), nullable=True)
expires_at = Column(DateTime, nullable=True) expires_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
# Связь с транзакцией # Связь с транзакцией
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True) transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
@@ -698,8 +698,8 @@ class PromoGroup(Base):
auto_assign_total_spent_kopeks = Column(Integer, nullable=True, default=None) auto_assign_total_spent_kopeks = Column(Integer, nullable=True, default=None)
apply_discounts_to_addons = Column(Boolean, nullable=False, default=True) apply_discounts_to_addons = Column(Boolean, nullable=False, default=True)
is_default = Column(Boolean, nullable=False, default=False) is_default = Column(Boolean, nullable=False, default=False)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
users = relationship('User', back_populates='promo_group') users = relationship('User', back_populates='promo_group')
user_promo_groups = relationship('UserPromoGroup', back_populates='promo_group', cascade='all, delete-orphan') user_promo_groups = relationship('UserPromoGroup', back_populates='promo_group', cascade='all, delete-orphan')
@@ -777,7 +777,7 @@ class UserPromoGroup(Base):
user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), primary_key=True) user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), primary_key=True)
promo_group_id = Column(Integer, ForeignKey('promo_groups.id', ondelete='CASCADE'), primary_key=True) promo_group_id = Column(Integer, ForeignKey('promo_groups.id', ondelete='CASCADE'), primary_key=True)
assigned_at = Column(DateTime, default=func.now()) assigned_at = Column(DateTime(timezone=True), default=func.now())
assigned_by = Column(String(50), default='system') assigned_by = Column(String(50), default='system')
user = relationship('User', back_populates='user_promo_groups') user = relationship('User', back_populates='user_promo_groups')
@@ -851,8 +851,8 @@ class Tariff(Base):
# Режим сброса трафика: DAY, WEEK, MONTH, NO_RESET (по умолчанию берётся из конфига) # Режим сброса трафика: DAY, WEEK, MONTH, NO_RESET (по умолчанию берётся из конфига)
traffic_reset_mode = Column(String(20), nullable=True, default=None) # None = использовать глобальную настройку traffic_reset_mode = Column(String(20), nullable=True, default=None) # None = использовать глобальную настройку
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
# M2M связь с промогруппами (какие промогруппы имеют доступ к тарифу) # M2M связь с промогруппами (какие промогруппы имеют доступ к тарифу)
allowed_promo_groups = relationship( allowed_promo_groups = relationship(
@@ -976,25 +976,25 @@ class User(Base):
has_had_paid_subscription = Column(Boolean, default=False, nullable=False) has_had_paid_subscription = Column(Boolean, default=False, nullable=False)
referred_by_id = Column(Integer, ForeignKey('users.id'), nullable=True) referred_by_id = Column(Integer, ForeignKey('users.id'), nullable=True)
referral_code = Column(String(20), unique=True, nullable=True) referral_code = Column(String(20), unique=True, nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
last_activity = Column(DateTime, default=func.now()) last_activity = Column(DateTime(timezone=True), default=func.now())
remnawave_uuid = Column(String(255), nullable=True, unique=True) remnawave_uuid = Column(String(255), nullable=True, unique=True)
# Cabinet authentication fields # Cabinet authentication fields
email = Column(String(255), unique=True, nullable=True, index=True) email = Column(String(255), unique=True, nullable=True, index=True)
email_verified = Column(Boolean, default=False, nullable=False) email_verified = Column(Boolean, default=False, nullable=False)
email_verified_at = Column(DateTime, nullable=True) email_verified_at = Column(DateTime(timezone=True), nullable=True)
password_hash = Column(String(255), nullable=True) password_hash = Column(String(255), nullable=True)
email_verification_token = Column(String(255), nullable=True) email_verification_token = Column(String(255), nullable=True)
email_verification_expires = Column(DateTime, nullable=True) email_verification_expires = Column(DateTime(timezone=True), nullable=True)
password_reset_token = Column(String(255), nullable=True) password_reset_token = Column(String(255), nullable=True)
password_reset_expires = Column(DateTime, nullable=True) password_reset_expires = Column(DateTime(timezone=True), nullable=True)
cabinet_last_login = Column(DateTime, nullable=True) cabinet_last_login = Column(DateTime(timezone=True), nullable=True)
# Email change fields # Email change fields
email_change_new = Column(String(255), nullable=True) # New email pending verification email_change_new = Column(String(255), nullable=True) # New email pending verification
email_change_code = Column(String(6), nullable=True) # 6-digit verification code email_change_code = Column(String(6), nullable=True) # 6-digit verification code
email_change_expires = Column(DateTime, nullable=True) # Code expiration email_change_expires = Column(DateTime(timezone=True), nullable=True) # Code expiration
# OAuth provider IDs # OAuth provider IDs
google_id = Column(String(255), unique=True, nullable=True, index=True) google_id = Column(String(255), unique=True, nullable=True, index=True)
yandex_id = Column(String(255), unique=True, nullable=True, index=True) yandex_id = Column(String(255), unique=True, nullable=True, index=True)
@@ -1013,8 +1013,8 @@ class User(Base):
referral_commission_percent = Column(Integer, nullable=True) referral_commission_percent = Column(Integer, nullable=True)
promo_offer_discount_percent = Column(Integer, nullable=False, default=0) promo_offer_discount_percent = Column(Integer, nullable=False, default=0)
promo_offer_discount_source = Column(String(100), nullable=True) promo_offer_discount_source = Column(String(100), nullable=True)
promo_offer_discount_expires_at = Column(DateTime, nullable=True) promo_offer_discount_expires_at = Column(DateTime(timezone=True), nullable=True)
last_remnawave_sync = Column(DateTime, nullable=True) last_remnawave_sync = Column(DateTime(timezone=True), nullable=True)
trojan_password = Column(String(255), nullable=True) trojan_password = Column(String(255), nullable=True)
vless_uuid = Column(String(255), nullable=True) vless_uuid = Column(String(255), nullable=True)
ss_password = Column(String(255), nullable=True) ss_password = Column(String(255), nullable=True)
@@ -1113,14 +1113,14 @@ class Subscription(Base):
status = Column(String(20), default=SubscriptionStatus.TRIAL.value) status = Column(String(20), default=SubscriptionStatus.TRIAL.value)
is_trial = Column(Boolean, default=True) is_trial = Column(Boolean, default=True)
start_date = Column(DateTime, default=func.now()) start_date = Column(DateTime(timezone=True), default=func.now())
end_date = Column(DateTime, nullable=False) end_date = Column(DateTime(timezone=True), nullable=False)
traffic_limit_gb = Column(Integer, default=0) traffic_limit_gb = Column(Integer, default=0)
traffic_used_gb = Column(Float, default=0.0) traffic_used_gb = Column(Float, default=0.0)
purchased_traffic_gb = Column(Integer, default=0) # Докупленный трафик purchased_traffic_gb = Column(Integer, default=0) # Докупленный трафик
traffic_reset_at = Column( traffic_reset_at = Column(
DateTime, nullable=True DateTime(timezone=True), nullable=True
) # Дата сброса докупленного трафика (30 дней после первой докупки) ) # Дата сброса докупленного трафика (30 дней после первой докупки)
subscription_url = Column(String, nullable=True) subscription_url = Column(String, nullable=True)
@@ -1134,10 +1134,10 @@ class Subscription(Base):
autopay_enabled = Column(Boolean, default=False) autopay_enabled = Column(Boolean, default=False)
autopay_days_before = Column(Integer, default=3) autopay_days_before = Column(Integer, default=3)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
last_webhook_update_at = Column(DateTime, nullable=True) last_webhook_update_at = Column(DateTime(timezone=True), nullable=True)
remnawave_short_uuid = Column(String(255), nullable=True) remnawave_short_uuid = Column(String(255), nullable=True)
@@ -1148,7 +1148,7 @@ class Subscription(Base):
is_daily_paused = Column( is_daily_paused = Column(
Boolean, default=False, nullable=False Boolean, default=False, nullable=False
) # Приостановлена ли суточная подписка пользователем ) # Приостановлена ли суточная подписка пользователем
last_daily_charge_at = Column(DateTime, nullable=True) # Время последнего суточного списания last_daily_charge_at = Column(DateTime(timezone=True), nullable=True) # Время последнего суточного списания
user = relationship('User', back_populates='subscription') user = relationship('User', back_populates='subscription')
tariff = relationship('Tariff', back_populates='subscriptions') tariff = relationship('Tariff', back_populates='subscriptions')
@@ -1162,7 +1162,7 @@ class Subscription(Base):
@property @property
def is_active(self) -> bool: def is_active(self) -> bool:
current_time = datetime.utcnow() current_time = datetime.now(UTC)
return ( return (
self.status == SubscriptionStatus.ACTIVE.value self.status == SubscriptionStatus.ACTIVE.value
and self.end_date is not None and self.end_date is not None
@@ -1172,11 +1172,11 @@ class Subscription(Base):
@property @property
def is_expired(self) -> bool: def is_expired(self) -> bool:
"""Проверяет, истёк ли срок подписки""" """Проверяет, истёк ли срок подписки"""
return self.end_date is not None and self.end_date <= datetime.utcnow() return self.end_date is not None and self.end_date <= datetime.now(UTC)
@property @property
def should_be_expired(self) -> bool: def should_be_expired(self) -> bool:
current_time = datetime.utcnow() current_time = datetime.now(UTC)
return ( return (
self.status == SubscriptionStatus.ACTIVE.value self.status == SubscriptionStatus.ACTIVE.value
and self.end_date is not None and self.end_date is not None
@@ -1185,7 +1185,7 @@ class Subscription(Base):
@property @property
def actual_status(self) -> str: def actual_status(self) -> str:
current_time = datetime.utcnow() current_time = datetime.now(UTC)
if self.status == SubscriptionStatus.EXPIRED.value: if self.status == SubscriptionStatus.EXPIRED.value:
return 'expired' return 'expired'
@@ -1208,7 +1208,6 @@ class Subscription(Base):
@property @property
def status_display(self) -> str: def status_display(self) -> str:
actual_status = self.actual_status actual_status = self.actual_status
datetime.utcnow()
if actual_status == 'expired': if actual_status == 'expired':
return '🔴 Истекла' return '🔴 Истекла'
@@ -1244,7 +1243,7 @@ class Subscription(Base):
def days_left(self) -> int: def days_left(self) -> int:
if self.end_date is None: if self.end_date is None:
return 0 return 0
current_time = datetime.utcnow() current_time = datetime.now(UTC)
if self.end_date <= current_time: if self.end_date <= current_time:
return 0 return 0
delta = self.end_date - current_time delta = self.end_date - current_time
@@ -1252,7 +1251,7 @@ class Subscription(Base):
@property @property
def time_left_display(self) -> str: def time_left_display(self) -> str:
current_time = datetime.utcnow() current_time = datetime.now(UTC)
if self.end_date <= current_time: if self.end_date <= current_time:
return 'истёк' return 'истёк'
@@ -1275,10 +1274,10 @@ class Subscription(Base):
return min((used / self.traffic_limit_gb) * 100, 100.0) return min((used / self.traffic_limit_gb) * 100, 100.0)
def extend_subscription(self, days: int): def extend_subscription(self, days: int):
if self.end_date > datetime.utcnow(): if self.end_date > datetime.now(UTC):
self.end_date = self.end_date + timedelta(days=days) self.end_date = self.end_date + timedelta(days=days)
else: else:
self.end_date = datetime.utcnow() + timedelta(days=days) self.end_date = datetime.now(UTC) + timedelta(days=days)
if self.status == SubscriptionStatus.EXPIRED.value: if self.status == SubscriptionStatus.EXPIRED.value:
self.status = SubscriptionStatus.ACTIVE.value self.status = SubscriptionStatus.ACTIVE.value
@@ -1323,16 +1322,16 @@ class TrafficPurchase(Base):
subscription_id = Column(Integer, ForeignKey('subscriptions.id', ondelete='CASCADE'), nullable=False, index=True) subscription_id = Column(Integer, ForeignKey('subscriptions.id', ondelete='CASCADE'), nullable=False, index=True)
traffic_gb = Column(Integer, nullable=False) # Количество ГБ в покупке traffic_gb = Column(Integer, nullable=False) # Количество ГБ в покупке
expires_at = Column(DateTime, nullable=False, index=True) # Дата истечения (покупка + 30 дней) expires_at = Column(DateTime(timezone=True), nullable=False, index=True) # Дата истечения (покупка + 30 дней)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
subscription = relationship('Subscription', back_populates='traffic_purchases') subscription = relationship('Subscription', back_populates='traffic_purchases')
@property @property
def is_expired(self) -> bool: def is_expired(self) -> bool:
"""Проверяет, истекла ли докупка.""" """Проверяет, истекла ли докупка."""
return datetime.utcnow() >= self.expires_at return datetime.now(UTC) >= self.expires_at
class Transaction(Base): class Transaction(Base):
@@ -1352,10 +1351,10 @@ class Transaction(Base):
# NaloGO чек # NaloGO чек
receipt_uuid = Column(String(255), nullable=True, index=True) receipt_uuid = Column(String(255), nullable=True, index=True)
receipt_created_at = Column(DateTime, nullable=True) receipt_created_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
completed_at = Column(DateTime, nullable=True) completed_at = Column(DateTime(timezone=True), nullable=True)
user = relationship('User', back_populates='transactions') user = relationship('User', back_populates='transactions')
@@ -1370,7 +1369,7 @@ class SubscriptionConversion(Base):
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey('users.id'), nullable=False) user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
converted_at = Column(DateTime, default=func.now()) converted_at = Column(DateTime(timezone=True), default=func.now())
trial_duration_days = Column(Integer, nullable=True) trial_duration_days = Column(Integer, nullable=True)
@@ -1380,7 +1379,7 @@ class SubscriptionConversion(Base):
first_paid_period_days = Column(Integer, nullable=True) first_paid_period_days = Column(Integer, nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
user = relationship('User', backref='subscription_conversions') user = relationship('User', backref='subscription_conversions')
@@ -1406,8 +1405,8 @@ class PromoCode(Base):
max_uses = Column(Integer, default=1) max_uses = Column(Integer, default=1)
current_uses = Column(Integer, default=0) current_uses = Column(Integer, default=0)
valid_from = Column(DateTime, default=func.now()) valid_from = Column(DateTime(timezone=True), default=func.now())
valid_until = Column(DateTime, nullable=True) valid_until = Column(DateTime(timezone=True), nullable=True)
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
first_purchase_only = Column(Boolean, default=False) # Только для первой покупки first_purchase_only = Column(Boolean, default=False) # Только для первой покупки
@@ -1415,15 +1414,15 @@ class PromoCode(Base):
created_by = Column(Integer, ForeignKey('users.id'), nullable=True) created_by = Column(Integer, ForeignKey('users.id'), nullable=True)
promo_group_id = Column(Integer, ForeignKey('promo_groups.id', ondelete='SET NULL'), nullable=True, index=True) promo_group_id = Column(Integer, ForeignKey('promo_groups.id', ondelete='SET NULL'), nullable=True, index=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
uses = relationship('PromoCodeUse', back_populates='promocode') uses = relationship('PromoCodeUse', back_populates='promocode')
promo_group = relationship('PromoGroup') promo_group = relationship('PromoGroup')
@property @property
def is_valid(self) -> bool: def is_valid(self) -> bool:
now = datetime.utcnow() now = datetime.now(UTC)
return ( return (
self.is_active self.is_active
and self.current_uses < self.max_uses and self.current_uses < self.max_uses
@@ -1443,7 +1442,7 @@ class PromoCodeUse(Base):
promocode_id = Column(Integer, ForeignKey('promocodes.id'), nullable=False) promocode_id = Column(Integer, ForeignKey('promocodes.id'), nullable=False)
user_id = Column(Integer, ForeignKey('users.id'), nullable=False) user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
used_at = Column(DateTime, default=func.now()) used_at = Column(DateTime(timezone=True), default=func.now())
promocode = relationship('PromoCode', back_populates='uses') promocode = relationship('PromoCode', back_populates='uses')
user = relationship('User') user = relationship('User')
@@ -1461,7 +1460,7 @@ class ReferralEarning(Base):
referral_transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True) referral_transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
user = relationship('User', foreign_keys=[user_id], back_populates='referral_earnings') user = relationship('User', foreign_keys=[user_id], back_populates='referral_earnings')
referral = relationship('User', foreign_keys=[referral_id]) referral = relationship('User', foreign_keys=[referral_id])
@@ -1502,11 +1501,11 @@ class WithdrawalRequest(Base):
# Обработка админом # Обработка админом
processed_by = Column(Integer, ForeignKey('users.id'), nullable=True) processed_by = Column(Integer, ForeignKey('users.id'), nullable=True)
processed_at = Column(DateTime, nullable=True) processed_at = Column(DateTime(timezone=True), nullable=True)
admin_comment = Column(Text, nullable=True) admin_comment = Column(Text, nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
user = relationship('User', foreign_keys=[user_id], backref='withdrawal_requests') user = relationship('User', foreign_keys=[user_id], backref='withdrawal_requests')
admin = relationship('User', foreign_keys=[processed_by]) admin = relationship('User', foreign_keys=[processed_by])
@@ -1524,18 +1523,18 @@ class ReferralContest(Base):
description = Column(Text, nullable=True) description = Column(Text, nullable=True)
prize_text = Column(Text, nullable=True) prize_text = Column(Text, nullable=True)
contest_type = Column(String(50), nullable=False, default='referral_paid') contest_type = Column(String(50), nullable=False, default='referral_paid')
start_at = Column(DateTime, nullable=False) start_at = Column(DateTime(timezone=True), nullable=False)
end_at = Column(DateTime, nullable=False) end_at = Column(DateTime(timezone=True), nullable=False)
daily_summary_time = Column(Time, nullable=False, default=time(hour=12, minute=0)) daily_summary_time = Column(Time, nullable=False, default=time(hour=12, minute=0))
daily_summary_times = Column(String(255), nullable=True) # CSV HH:MM daily_summary_times = Column(String(255), nullable=True) # CSV HH:MM
timezone = Column(String(64), nullable=False, default='UTC') timezone = Column(String(64), nullable=False, default='UTC')
is_active = Column(Boolean, nullable=False, default=True) is_active = Column(Boolean, nullable=False, default=True)
last_daily_summary_date = Column(Date, nullable=True) last_daily_summary_date = Column(Date, nullable=True)
last_daily_summary_at = Column(DateTime, nullable=True) last_daily_summary_at = Column(DateTime(timezone=True), nullable=True)
final_summary_sent = Column(Boolean, nullable=False, default=False) final_summary_sent = Column(Boolean, nullable=False, default=False)
created_by = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True) created_by = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
creator = relationship('User', backref='created_referral_contests') creator = relationship('User', backref='created_referral_contests')
events = relationship( events = relationship(
@@ -1565,7 +1564,7 @@ class ReferralContestEvent(Base):
referral_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), nullable=False) referral_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), nullable=False)
event_type = Column(String(50), nullable=False) event_type = Column(String(50), nullable=False)
amount_kopeks = Column(Integer, nullable=False, default=0) amount_kopeks = Column(Integer, nullable=False, default=0)
occurred_at = Column(DateTime, nullable=False, default=func.now()) occurred_at = Column(DateTime(timezone=True), nullable=False, default=func.now())
contest = relationship('ReferralContest', back_populates='events') contest = relationship('ReferralContest', back_populates='events')
referrer = relationship('User', foreign_keys=[referrer_id]) referrer = relationship('User', foreign_keys=[referrer_id])
@@ -1585,7 +1584,7 @@ class ReferralContestVirtualParticipant(Base):
display_name = Column(String(255), nullable=False) display_name = Column(String(255), nullable=False)
referral_count = Column(Integer, nullable=False, default=0) referral_count = Column(Integer, nullable=False, default=0)
total_amount_kopeks = Column(Integer, nullable=False, default=0) total_amount_kopeks = Column(Integer, nullable=False, default=0)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
contest = relationship('ReferralContest') contest = relationship('ReferralContest')
@@ -1611,8 +1610,8 @@ class ContestTemplate(Base):
cooldown_hours = Column(Integer, nullable=False, default=24) cooldown_hours = Column(Integer, nullable=False, default=24)
payload = Column(JSON, nullable=True) payload = Column(JSON, nullable=True)
is_enabled = Column(Boolean, nullable=False, default=True) is_enabled = Column(Boolean, nullable=False, default=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
rounds = relationship('ContestRound', back_populates='template') rounds = relationship('ContestRound', back_populates='template')
@@ -1626,8 +1625,8 @@ class ContestRound(Base):
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
template_id = Column(Integer, ForeignKey('contest_templates.id', ondelete='CASCADE'), nullable=False) template_id = Column(Integer, ForeignKey('contest_templates.id', ondelete='CASCADE'), nullable=False)
starts_at = Column(DateTime, nullable=False) starts_at = Column(DateTime(timezone=True), nullable=False)
ends_at = Column(DateTime, nullable=False) ends_at = Column(DateTime(timezone=True), nullable=False)
status = Column(String(20), nullable=False, default='active') # active, finished status = Column(String(20), nullable=False, default='active') # active, finished
payload = Column(JSON, nullable=True) payload = Column(JSON, nullable=True)
winners_count = Column(Integer, nullable=False, default=0) winners_count = Column(Integer, nullable=False, default=0)
@@ -1635,8 +1634,8 @@ class ContestRound(Base):
attempts_per_user = Column(Integer, nullable=False, default=1) attempts_per_user = Column(Integer, nullable=False, default=1)
message_id = Column(BigInteger, nullable=True) message_id = Column(BigInteger, nullable=True)
chat_id = Column(BigInteger, nullable=True) chat_id = Column(BigInteger, nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
template = relationship('ContestTemplate', back_populates='rounds') template = relationship('ContestTemplate', back_populates='rounds')
attempts = relationship('ContestAttempt', back_populates='round', cascade='all, delete-orphan') attempts = relationship('ContestAttempt', back_populates='round', cascade='all, delete-orphan')
@@ -1654,7 +1653,7 @@ class ContestAttempt(Base):
user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), nullable=False) user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), nullable=False)
answer = Column(Text, nullable=True) answer = Column(Text, nullable=True)
is_winner = Column(Boolean, nullable=False, default=False) is_winner = Column(Boolean, nullable=False, default=False)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
round = relationship('ContestRound', back_populates='attempts') round = relationship('ContestRound', back_populates='attempts')
user = relationship('User') user = relationship('User')
@@ -1674,8 +1673,8 @@ class Squad(Base):
description = Column(Text, nullable=True) description = Column(Text, nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
@property @property
def price_rubles(self) -> float: def price_rubles(self) -> float:
@@ -1696,8 +1695,8 @@ class ServiceRule(Base):
language = Column(String(5), default='ru') language = Column(String(5), default='ru')
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
class PrivacyPolicy(Base): class PrivacyPolicy(Base):
@@ -1707,8 +1706,8 @@ class PrivacyPolicy(Base):
language = Column(String(10), nullable=False, unique=True) language = Column(String(10), nullable=False, unique=True)
content = Column(Text, nullable=False) content = Column(Text, nullable=False)
is_enabled = Column(Boolean, default=True, nullable=False) is_enabled = Column(Boolean, default=True, nullable=False)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
class PublicOffer(Base): class PublicOffer(Base):
@@ -1718,8 +1717,8 @@ class PublicOffer(Base):
language = Column(String(10), nullable=False, unique=True) language = Column(String(10), nullable=False, unique=True)
content = Column(Text, nullable=False) content = Column(Text, nullable=False)
is_enabled = Column(Boolean, default=True, nullable=False) is_enabled = Column(Boolean, default=True, nullable=False)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
class FaqSetting(Base): class FaqSetting(Base):
@@ -1728,8 +1727,8 @@ class FaqSetting(Base):
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
language = Column(String(10), nullable=False, unique=True) language = Column(String(10), nullable=False, unique=True)
is_enabled = Column(Boolean, default=True, nullable=False) is_enabled = Column(Boolean, default=True, nullable=False)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
class FaqPage(Base): class FaqPage(Base):
@@ -1741,8 +1740,8 @@ class FaqPage(Base):
content = Column(Text, nullable=False) content = Column(Text, nullable=False)
display_order = Column(Integer, default=0, nullable=False) display_order = Column(Integer, default=0, nullable=False)
is_active = Column(Boolean, default=True, nullable=False) is_active = Column(Boolean, default=True, nullable=False)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
class SystemSetting(Base): class SystemSetting(Base):
@@ -1753,8 +1752,8 @@ class SystemSetting(Base):
value = Column(Text, nullable=True) value = Column(Text, nullable=True)
description = Column(Text, nullable=True) description = Column(Text, nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
class MonitoringLog(Base): class MonitoringLog(Base):
@@ -1769,7 +1768,7 @@ class MonitoringLog(Base):
is_success = Column(Boolean, default=True) is_success = Column(Boolean, default=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
class SentNotification(Base): class SentNotification(Base):
@@ -1780,7 +1779,7 @@ class SentNotification(Base):
subscription_id = Column(Integer, ForeignKey('subscriptions.id', ondelete='CASCADE'), nullable=False) subscription_id = Column(Integer, ForeignKey('subscriptions.id', ondelete='CASCADE'), nullable=False)
notification_type = Column(String(50), nullable=False) notification_type = Column(String(50), nullable=False)
days_before = Column(Integer, nullable=True) days_before = Column(Integer, nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
user = relationship('User', backref='sent_notifications') user = relationship('User', backref='sent_notifications')
subscription = relationship('Subscription', backref=backref('sent_notifications', passive_deletes=True)) subscription = relationship('Subscription', backref=backref('sent_notifications', passive_deletes=True))
@@ -1797,9 +1796,9 @@ class SubscriptionEvent(Base):
amount_kopeks = Column(Integer, nullable=True) amount_kopeks = Column(Integer, nullable=True)
currency = Column(String(16), nullable=True) currency = Column(String(16), nullable=True)
message = Column(Text, nullable=True) message = Column(Text, nullable=True)
occurred_at = Column(DateTime, nullable=False, default=func.now()) occurred_at = Column(DateTime(timezone=True), nullable=False, default=func.now())
extra = Column(JSON, nullable=True) extra = Column(JSON, nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
user = relationship('User', backref='subscription_events') user = relationship('User', backref='subscription_events')
subscription = relationship('Subscription', backref='subscription_events') subscription = relationship('Subscription', backref='subscription_events')
@@ -1816,13 +1815,13 @@ class DiscountOffer(Base):
notification_type = Column(String(50), nullable=False) notification_type = Column(String(50), nullable=False)
discount_percent = Column(Integer, nullable=False, default=0) discount_percent = Column(Integer, nullable=False, default=0)
bonus_amount_kopeks = Column(Integer, nullable=False, default=0) bonus_amount_kopeks = Column(Integer, nullable=False, default=0)
expires_at = Column(DateTime, nullable=False) expires_at = Column(DateTime(timezone=True), nullable=False)
claimed_at = Column(DateTime, nullable=True) claimed_at = Column(DateTime(timezone=True), nullable=True)
is_active = Column(Boolean, default=True, nullable=False) is_active = Column(Boolean, default=True, nullable=False)
effect_type = Column(String(50), nullable=False, default='percent_discount') effect_type = Column(String(50), nullable=False, default='percent_discount')
extra_data = Column(JSON, nullable=True) extra_data = Column(JSON, nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
user = relationship('User', back_populates='discount_offers') user = relationship('User', back_populates='discount_offers')
subscription = relationship('Subscription', back_populates='discount_offers') subscription = relationship('Subscription', back_populates='discount_offers')
@@ -1846,8 +1845,8 @@ class PromoOfferTemplate(Base):
test_squad_uuids = Column(JSON, default=list) test_squad_uuids = Column(JSON, default=list)
is_active = Column(Boolean, default=True, nullable=False) is_active = Column(Boolean, default=True, nullable=False)
created_by = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True) created_by = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
creator = relationship('User') creator = relationship('User')
@@ -1859,9 +1858,9 @@ class SubscriptionTemporaryAccess(Base):
subscription_id = Column(Integer, ForeignKey('subscriptions.id', ondelete='CASCADE'), nullable=False) subscription_id = Column(Integer, ForeignKey('subscriptions.id', ondelete='CASCADE'), nullable=False)
offer_id = Column(Integer, ForeignKey('discount_offers.id', ondelete='CASCADE'), nullable=False) offer_id = Column(Integer, ForeignKey('discount_offers.id', ondelete='CASCADE'), nullable=False)
squad_uuid = Column(String(255), nullable=False) squad_uuid = Column(String(255), nullable=False)
expires_at = Column(DateTime, nullable=False) expires_at = Column(DateTime(timezone=True), nullable=False)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
deactivated_at = Column(DateTime, nullable=True) deactivated_at = Column(DateTime(timezone=True), nullable=True)
is_active = Column(Boolean, default=True, nullable=False) is_active = Column(Boolean, default=True, nullable=False)
was_already_connected = Column(Boolean, default=False, nullable=False) was_already_connected = Column(Boolean, default=False, nullable=False)
@@ -1880,7 +1879,7 @@ class PromoOfferLog(Base):
percent = Column(Integer, nullable=True) percent = Column(Integer, nullable=True)
effect_type = Column(String(50), nullable=True) effect_type = Column(String(50), nullable=True)
details = Column(JSON, nullable=True) details = Column(JSON, nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
user = relationship('User', back_populates='promo_offer_logs') user = relationship('User', back_populates='promo_offer_logs')
offer = relationship('DiscountOffer', back_populates='logs') offer = relationship('DiscountOffer', back_populates='logs')
@@ -1922,8 +1921,8 @@ class Poll(Base):
reward_enabled = Column(Boolean, nullable=False, default=False) reward_enabled = Column(Boolean, nullable=False, default=False)
reward_amount_kopeks = Column(Integer, nullable=False, default=0) reward_amount_kopeks = Column(Integer, nullable=False, default=0)
created_by = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True) created_by = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True)
created_at = Column(DateTime, default=func.now(), nullable=False) created_at = Column(DateTime(timezone=True), default=func.now(), nullable=False)
updated_at = Column(DateTime, default=func.now(), onupdate=func.now(), nullable=False) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now(), nullable=False)
creator = relationship('User', backref='created_polls', foreign_keys=[created_by]) creator = relationship('User', backref='created_polls', foreign_keys=[created_by])
questions = relationship( questions = relationship(
@@ -1975,9 +1974,9 @@ class PollResponse(Base):
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
poll_id = Column(Integer, ForeignKey('polls.id', ondelete='CASCADE'), nullable=False, index=True) poll_id = Column(Integer, ForeignKey('polls.id', ondelete='CASCADE'), nullable=False, index=True)
user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), nullable=False, index=True) user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), nullable=False, index=True)
sent_at = Column(DateTime, default=func.now(), nullable=False) sent_at = Column(DateTime(timezone=True), default=func.now(), nullable=False)
started_at = Column(DateTime, nullable=True) started_at = Column(DateTime(timezone=True), nullable=True)
completed_at = Column(DateTime, nullable=True) completed_at = Column(DateTime(timezone=True), nullable=True)
reward_given = Column(Boolean, nullable=False, default=False) reward_given = Column(Boolean, nullable=False, default=False)
reward_amount_kopeks = Column(Integer, nullable=False, default=0) reward_amount_kopeks = Column(Integer, nullable=False, default=0)
@@ -1999,7 +1998,7 @@ class PollAnswer(Base):
response_id = Column(Integer, ForeignKey('poll_responses.id', ondelete='CASCADE'), nullable=False, index=True) response_id = Column(Integer, ForeignKey('poll_responses.id', ondelete='CASCADE'), nullable=False, index=True)
question_id = Column(Integer, ForeignKey('poll_questions.id', ondelete='CASCADE'), nullable=False, index=True) question_id = Column(Integer, ForeignKey('poll_questions.id', ondelete='CASCADE'), nullable=False, index=True)
option_id = Column(Integer, ForeignKey('poll_options.id', ondelete='CASCADE'), nullable=False, index=True) option_id = Column(Integer, ForeignKey('poll_options.id', ondelete='CASCADE'), nullable=False, index=True)
created_at = Column(DateTime, default=func.now(), nullable=False) created_at = Column(DateTime(timezone=True), default=func.now(), nullable=False)
response = relationship('PollResponse', back_populates='answers') response = relationship('PollResponse', back_populates='answers')
question = relationship('PollQuestion', back_populates='answers') question = relationship('PollQuestion', back_populates='answers')
@@ -2033,8 +2032,8 @@ class ServerSquad(Base):
max_users = Column(Integer, nullable=True) max_users = Column(Integer, nullable=True)
current_users = Column(Integer, default=0) current_users = Column(Integer, default=0)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
allowed_promo_groups = relationship( allowed_promo_groups = relationship(
'PromoGroup', 'PromoGroup',
@@ -2069,7 +2068,7 @@ class SubscriptionServer(Base):
subscription_id = Column(Integer, ForeignKey('subscriptions.id'), nullable=False) subscription_id = Column(Integer, ForeignKey('subscriptions.id'), nullable=False)
server_squad_id = Column(Integer, ForeignKey('server_squads.id'), nullable=False) server_squad_id = Column(Integer, ForeignKey('server_squads.id'), nullable=False)
connected_at = Column(DateTime, default=func.now()) connected_at = Column(DateTime(timezone=True), default=func.now())
paid_price_kopeks = Column(Integer, default=0) paid_price_kopeks = Column(Integer, default=0)
@@ -2088,7 +2087,7 @@ class SupportAuditLog(Base):
ticket_id = Column(Integer, ForeignKey('tickets.id', ondelete='SET NULL'), nullable=True) ticket_id = Column(Integer, ForeignKey('tickets.id', ondelete='SET NULL'), nullable=True)
target_user_id = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True) target_user_id = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True)
details = Column(JSON, nullable=True) details = Column(JSON, nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
actor = relationship('User', foreign_keys=[actor_user_id]) actor = relationship('User', foreign_keys=[actor_user_id])
ticket = relationship('Ticket', foreign_keys=[ticket_id]) ticket = relationship('Ticket', foreign_keys=[ticket_id])
@@ -2101,8 +2100,8 @@ class UserMessage(Base):
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
sort_order = Column(Integer, default=0) sort_order = Column(Integer, default=0)
created_by = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True) created_by = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
creator = relationship('User', backref='created_messages') creator = relationship('User', backref='created_messages')
def __repr__(self): def __repr__(self):
@@ -2117,8 +2116,8 @@ class WelcomeText(Base):
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
is_enabled = Column(Boolean, default=True) is_enabled = Column(Boolean, default=True)
created_by = Column(Integer, ForeignKey('users.id'), nullable=True) created_by = Column(Integer, ForeignKey('users.id'), nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
creator = relationship('User', backref='created_welcome_texts') creator = relationship('User', backref='created_welcome_texts')
@@ -2134,8 +2133,8 @@ class PinnedMessage(Base):
send_on_every_start = Column(Boolean, nullable=False, server_default='1', default=True) send_on_every_start = Column(Boolean, nullable=False, server_default='1', default=True)
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
created_by = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True) created_by = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
creator = relationship('User', backref='pinned_messages') creator = relationship('User', backref='pinned_messages')
@@ -2162,8 +2161,8 @@ class AdvertisingCampaign(Base):
is_active = Column(Boolean, default=True) is_active = Column(Boolean, default=True)
created_by = Column(Integer, ForeignKey('users.id'), nullable=True) created_by = Column(Integer, ForeignKey('users.id'), nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
registrations = relationship('AdvertisingCampaignRegistration', back_populates='campaign') registrations = relationship('AdvertisingCampaignRegistration', back_populates='campaign')
tariff = relationship('Tariff', foreign_keys=[tariff_id]) tariff = relationship('Tariff', foreign_keys=[tariff_id])
@@ -2203,7 +2202,7 @@ class AdvertisingCampaignRegistration(Base):
tariff_id = Column(Integer, ForeignKey('tariffs.id', ondelete='SET NULL'), nullable=True) tariff_id = Column(Integer, ForeignKey('tariffs.id', ondelete='SET NULL'), nullable=True)
tariff_duration_days = Column(Integer, nullable=True) tariff_duration_days = Column(Integer, nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
campaign = relationship('AdvertisingCampaign', back_populates='registrations') campaign = relationship('AdvertisingCampaign', back_populates='registrations')
user = relationship('User') user = relationship('User')
@@ -2232,13 +2231,13 @@ class Ticket(Base):
priority = Column(String(20), default='normal', nullable=False) # low, normal, high, urgent priority = Column(String(20), default='normal', nullable=False) # low, normal, high, urgent
# Блокировка ответов пользователя в этом тикете # Блокировка ответов пользователя в этом тикете
user_reply_block_permanent = Column(Boolean, default=False, nullable=False) user_reply_block_permanent = Column(Boolean, default=False, nullable=False)
user_reply_block_until = Column(DateTime, nullable=True) user_reply_block_until = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
closed_at = Column(DateTime, nullable=True) closed_at = Column(DateTime(timezone=True), nullable=True)
# SLA reminders # SLA reminders
last_sla_reminder_at = Column(DateTime, nullable=True) last_sla_reminder_at = Column(DateTime(timezone=True), nullable=True)
# Связи # Связи
user = relationship('User', backref='tickets') user = relationship('User', backref='tickets')
@@ -2266,9 +2265,9 @@ class Ticket(Base):
return True return True
if self.user_reply_block_until: if self.user_reply_block_until:
try: try:
from datetime import datetime from datetime import UTC, datetime
return self.user_reply_block_until > datetime.utcnow() return self.user_reply_block_until > datetime.now(UTC)
except Exception: except Exception:
return True return True
return False return False
@@ -2308,7 +2307,7 @@ class TicketMessage(Base):
media_file_id = Column(String(255), nullable=True) media_file_id = Column(String(255), nullable=True)
media_caption = Column(Text, nullable=True) media_caption = Column(Text, nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
# Связи # Связи
ticket = relationship('Ticket', back_populates='messages') ticket = relationship('Ticket', back_populates='messages')
@@ -2334,10 +2333,10 @@ class WebApiToken(Base):
token_hash = Column(String(128), nullable=False, unique=True, index=True) token_hash = Column(String(128), nullable=False, unique=True, index=True)
token_prefix = Column(String(32), nullable=False, index=True) token_prefix = Column(String(32), nullable=False, index=True)
description = Column(Text, nullable=True) description = Column(Text, nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
expires_at = Column(DateTime, nullable=True) expires_at = Column(DateTime(timezone=True), nullable=True)
last_used_at = Column(DateTime, nullable=True) last_used_at = Column(DateTime(timezone=True), nullable=True)
last_used_ip = Column(String(64), nullable=True) last_used_ip = Column(String(64), nullable=True)
is_active = Column(Boolean, default=True, nullable=False) is_active = Column(Boolean, default=True, nullable=False)
created_by = Column(String(255), nullable=True) created_by = Column(String(255), nullable=True)
@@ -2357,8 +2356,8 @@ class MainMenuButton(Base):
visibility = Column(String(20), nullable=False, default=MainMenuButtonVisibility.ALL.value) visibility = Column(String(20), nullable=False, default=MainMenuButtonVisibility.ALL.value)
is_active = Column(Boolean, nullable=False, default=True) is_active = Column(Boolean, nullable=False, default=True)
display_order = Column(Integer, nullable=False, default=0) display_order = Column(Integer, nullable=False, default=0)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
__table_args__ = (Index('ix_main_menu_buttons_order', 'display_order', 'id'),) __table_args__ = (Index('ix_main_menu_buttons_order', 'display_order', 'id'),)
@@ -2393,7 +2392,7 @@ class MenuLayoutHistory(Base):
action = Column(String(50), nullable=False) # update, reset, import action = Column(String(50), nullable=False) # update, reset, import
changes_summary = Column(Text, nullable=True) # Краткое описание изменений changes_summary = Column(Text, nullable=True) # Краткое описание изменений
user_info = Column(String(255), nullable=True) # Информация о пользователе/токене user_info = Column(String(255), nullable=True) # Информация о пользователе/токене
created_at = Column(DateTime, default=func.now(), index=True) created_at = Column(DateTime(timezone=True), default=func.now(), index=True)
__table_args__ = (Index('ix_menu_layout_history_created', 'created_at'),) __table_args__ = (Index('ix_menu_layout_history_created', 'created_at'),)
@@ -2410,7 +2409,7 @@ class ButtonClickLog(Base):
button_id = Column(String(100), nullable=False, index=True) # ID кнопки button_id = Column(String(100), nullable=False, index=True) # ID кнопки
user_id = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True, index=True) user_id = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True, index=True)
callback_data = Column(String(255), nullable=True) # callback_data кнопки callback_data = Column(String(255), nullable=True) # callback_data кнопки
clicked_at = Column(DateTime, default=func.now(), index=True) clicked_at = Column(DateTime(timezone=True), default=func.now(), index=True)
# Дополнительная информация # Дополнительная информация
button_type = Column(String(20), nullable=True, index=True) # builtin, callback, url, mini_app button_type = Column(String(20), nullable=True, index=True) # builtin, callback, url, mini_app
@@ -2444,9 +2443,9 @@ class Webhook(Base):
event_type = Column(String(50), nullable=False) # user.created, payment.completed, ticket.created, etc. event_type = Column(String(50), nullable=False) # user.created, payment.completed, ticket.created, etc.
is_active = Column(Boolean, default=True, nullable=False) is_active = Column(Boolean, default=True, nullable=False)
description = Column(Text, nullable=True) description = Column(Text, nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
last_triggered_at = Column(DateTime, nullable=True) last_triggered_at = Column(DateTime(timezone=True), nullable=True)
failure_count = Column(Integer, default=0, nullable=False) failure_count = Column(Integer, default=0, nullable=False)
success_count = Column(Integer, default=0, nullable=False) success_count = Column(Integer, default=0, nullable=False)
@@ -2475,9 +2474,9 @@ class WebhookDelivery(Base):
status = Column(String(20), nullable=False) # pending, success, failed status = Column(String(20), nullable=False) # pending, success, failed
error_message = Column(Text, nullable=True) error_message = Column(Text, nullable=True)
attempt_number = Column(Integer, default=1, nullable=False) attempt_number = Column(Integer, default=1, nullable=False)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
delivered_at = Column(DateTime, nullable=True) delivered_at = Column(DateTime(timezone=True), nullable=True)
next_retry_at = Column(DateTime, nullable=True) next_retry_at = Column(DateTime(timezone=True), nullable=True)
webhook = relationship('Webhook', back_populates='deliveries') webhook = relationship('Webhook', back_populates='deliveries')
@@ -2495,15 +2494,15 @@ class CabinetRefreshToken(Base):
user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), nullable=False) user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), nullable=False)
token_hash = Column(String(255), unique=True, nullable=False, index=True) token_hash = Column(String(255), unique=True, nullable=False, index=True)
device_info = Column(String(500), nullable=True) device_info = Column(String(500), nullable=True)
expires_at = Column(DateTime, nullable=False) expires_at = Column(DateTime(timezone=True), nullable=False)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
revoked_at = Column(DateTime, nullable=True) revoked_at = Column(DateTime(timezone=True), nullable=True)
user = relationship('User', backref='cabinet_tokens') user = relationship('User', backref='cabinet_tokens')
@property @property
def is_expired(self) -> bool: def is_expired(self) -> bool:
return datetime.utcnow() > self.expires_at return datetime.now(UTC) > self.expires_at
@property @property
def is_revoked(self) -> bool: def is_revoked(self) -> bool:
@@ -2549,8 +2548,8 @@ class WheelConfig(Base):
promo_prefix = Column(String(20), default='WHEEL', nullable=False) promo_prefix = Column(String(20), default='WHEEL', nullable=False)
promo_validity_days = Column(Integer, default=7, nullable=False) promo_validity_days = Column(Integer, default=7, nullable=False)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
prizes = relationship('WheelPrize', back_populates='config', cascade='all, delete-orphan') prizes = relationship('WheelPrize', back_populates='config', cascade='all, delete-orphan')
@@ -2588,8 +2587,8 @@ class WheelPrize(Base):
promo_subscription_days = Column(Integer, default=0) promo_subscription_days = Column(Integer, default=0)
promo_traffic_gb = Column(Integer, default=0) promo_traffic_gb = Column(Integer, default=0)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
config = relationship('WheelConfig', back_populates='prizes') config = relationship('WheelConfig', back_populates='prizes')
spins = relationship('WheelSpin', back_populates='prize') spins = relationship('WheelSpin', back_populates='prize')
@@ -2624,9 +2623,9 @@ class WheelSpin(Base):
# Флаг успешного начисления # Флаг успешного начисления
is_applied = Column(Boolean, default=False, nullable=False) is_applied = Column(Boolean, default=False, nullable=False)
applied_at = Column(DateTime, nullable=True) applied_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
user = relationship('User', backref='wheel_spins') user = relationship('User', backref='wheel_spins')
prize = relationship('WheelPrize', back_populates='spins') prize = relationship('WheelPrize', back_populates='spins')
@@ -2671,8 +2670,8 @@ class TicketNotification(Base):
# Прочитано ли уведомление # Прочитано ли уведомление
is_read = Column(Boolean, default=False, nullable=False) is_read = Column(Boolean, default=False, nullable=False)
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
read_at = Column(DateTime, nullable=True) read_at = Column(DateTime(timezone=True), nullable=True)
ticket = relationship('Ticket', backref='notifications') ticket = relationship('Ticket', backref='notifications')
user = relationship('User', backref='ticket_notifications') user = relationship('User', backref='ticket_notifications')
@@ -2729,8 +2728,8 @@ class PaymentMethodConfig(Base):
lazy='selectin', lazy='selectin',
) )
created_at = Column(DateTime, default=func.now()) created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<PaymentMethodConfig method_id='{self.method_id}' order={self.sort_order} enabled={self.is_enabled}>" return f"<PaymentMethodConfig method_id='{self.method_id}' order={self.sort_order} enabled={self.is_enabled}>"
+66 -2
View File
@@ -1,4 +1,4 @@
from datetime import datetime from datetime import UTC, datetime
import structlog import structlog
from sqlalchemy import select, text from sqlalchemy import select, text
@@ -4720,7 +4720,7 @@ async def ensure_default_web_api_token() -> bool:
updated = True updated = True
if updated: if updated:
existing.updated_at = datetime.utcnow() existing.updated_at = datetime.now(UTC)
await session.commit() await session.commit()
return True return True
@@ -6454,6 +6454,63 @@ async def migrate_cloudpayments_transaction_id_to_bigint() -> bool:
return False return False
async def migrate_datetime_to_timestamptz() -> bool:
"""Migrate all TIMESTAMP WITHOUT TIME ZONE columns to TIMESTAMP WITH TIME ZONE.
PostgreSQL treats existing naive values as UTC (session timezone) during conversion,
so existing data is preserved correctly.
SQLite does not have a separate timestamptz type, so this is a no-op.
"""
db_type = await get_database_type()
if db_type == 'sqlite':
logger.info('ℹ️ SQLite не требует миграции TIMESTAMPTZ')
return True
if db_type != 'postgresql':
logger.info('ℹ️ Миграция TIMESTAMPTZ поддерживается только для PostgreSQL', db_type=db_type)
return True
try:
async with engine.begin() as conn:
result = await conn.execute(
text("""
SELECT table_name, column_name
FROM information_schema.columns
WHERE table_schema = 'public'
AND data_type = 'timestamp without time zone'
ORDER BY table_name, column_name
""")
)
columns = result.fetchall()
if not columns:
logger.info('ℹ️ Все DateTime колонки уже TIMESTAMPTZ')
return True
logger.info(
'🔄 Найдено колонок для миграции на TIMESTAMPTZ',
count=len(columns),
)
await conn.execute(text("SET LOCAL timezone = 'UTC'"))
for table_name, column_name in columns:
await conn.execute(
text(f'ALTER TABLE "{table_name}" ALTER COLUMN "{column_name}" TYPE TIMESTAMP WITH TIME ZONE')
)
logger.info(
'✅ Мигрировано колонок на TIMESTAMPTZ',
count=len(columns),
)
return True
except Exception as error:
logger.error('❌ Ошибка миграции DateTime колонок на TIMESTAMPTZ', error=error)
return False
async def run_universal_migration(): async def run_universal_migration():
logger.info('=== НАЧАЛО УНИВЕРСАЛЬНОЙ МИГРАЦИИ ===') logger.info('=== НАЧАЛО УНИВЕРСАЛЬНОЙ МИГРАЦИИ ===')
@@ -7130,6 +7187,13 @@ async def run_universal_migration():
else: else:
logger.warning('⚠️ Проблемы с колонкой last_webhook_update_at') logger.warning('⚠️ Проблемы с колонкой last_webhook_update_at')
logger.info('=== МИГРАЦИЯ DATETIME КОЛОНОК НА TIMESTAMPTZ ===')
timestamptz_ready = await migrate_datetime_to_timestamptz()
if timestamptz_ready:
logger.info('✅ Все DateTime колонки мигрированы на TIMESTAMPTZ')
else:
logger.warning('⚠️ Проблемы с миграцией DateTime колонок')
async with engine.begin() as conn: async with engine.begin() as conn:
total_subs = await conn.execute(text('SELECT COUNT(*) FROM subscriptions')) total_subs = await conn.execute(text('SELECT COUNT(*) FROM subscriptions'))
unique_users = await conn.execute(text('SELECT COUNT(DISTINCT user_id) FROM subscriptions')) unique_users = await conn.execute(text('SELECT COUNT(DISTINCT user_id) FROM subscriptions'))
+4 -4
View File
@@ -3,7 +3,7 @@ from __future__ import annotations
import asyncio import asyncio
import base64 import base64
import json import json
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
from typing import Any from typing import Any
import aiohttp import aiohttp
@@ -33,12 +33,12 @@ class WataPublicKeyProvider:
async def get_public_key(self) -> str | None: async def get_public_key(self) -> str | None:
"""Returns a cached public key or fetches a new one from WATA.""" """Returns a cached public key or fetches a new one from WATA."""
now = datetime.utcnow() now = datetime.now(UTC)
if self._cached_key and self._expires_at and now < self._expires_at: if self._cached_key and self._expires_at and now < self._expires_at:
return self._cached_key return self._cached_key
async with self._lock: async with self._lock:
now = datetime.utcnow() now = datetime.now(UTC)
if self._cached_key and self._expires_at and now < self._expires_at: if self._cached_key and self._expires_at and now < self._expires_at:
return self._cached_key return self._cached_key
@@ -46,7 +46,7 @@ class WataPublicKeyProvider:
if key: if key:
self._cached_key = key self._cached_key = key
if self._cache_seconds > 0: if self._cache_seconds > 0:
self._expires_at = datetime.utcnow() + timedelta(seconds=self._cache_seconds) self._expires_at = datetime.now(UTC) + timedelta(seconds=self._cache_seconds)
else: else:
self._expires_at = None self._expires_at = None
logger.debug('Получен и закеширован публичный ключ WATA') logger.debug('Получен и закеширован публичный ключ WATA')
+3 -3
View File
@@ -3,7 +3,7 @@ import io
import math import math
import time import time
from collections.abc import Iterable from collections.abc import Iterable
from datetime import datetime from datetime import UTC, datetime
from aiogram import Dispatcher, F, types from aiogram import Dispatcher, F, types
from aiogram.filters import BaseFilter, StateFilter from aiogram.filters import BaseFilter, StateFilter
@@ -742,7 +742,7 @@ async def export_settings(
keys = sorted(set(keys)) keys = sorted(set(keys))
lines = [ lines = [
'# RemnaWave bot configuration export', '# RemnaWave bot configuration export',
f'# Generated at {datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC")}', f'# Generated at {datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S UTC")}',
] ]
for setting_key in keys: for setting_key in keys:
@@ -753,7 +753,7 @@ async def export_settings(
lines.append(f'{setting_key}={raw_value}') lines.append(f'{setting_key}={raw_value}')
content = '\n'.join(lines) content = '\n'.join(lines)
filename = f'bot-settings-{datetime.utcnow().strftime("%Y%m%d-%H%M%S")}.env' filename = f'bot-settings-{datetime.now(UTC).strftime("%Y%m%d-%H%M%S")}.env'
file = types.BufferedInputFile(content.encode('utf-8'), filename=filename) file = types.BufferedInputFile(content.encode('utf-8'), filename=filename)
await callback.message.answer_document( await callback.message.answer_document(
+3 -3
View File
@@ -399,7 +399,7 @@ async def delete_contest(
await callback.answer(texts.t('ADMIN_CONTEST_NOT_FOUND', 'Конкурс не найден.'), show_alert=True) await callback.answer(texts.t('ADMIN_CONTEST_NOT_FOUND', 'Конкурс не найден.'), show_alert=True)
return return
now_utc = datetime.utcnow() now_utc = datetime.now(UTC)
if contest.is_active or contest.end_at > now_utc: if contest.is_active or contest.end_at > now_utc:
await callback.answer( await callback.answer(
texts.t('ADMIN_CONTEST_DELETE_RESTRICT', 'Удалять можно только завершённые конкурсы.'), texts.t('ADMIN_CONTEST_DELETE_RESTRICT', 'Удалять можно только завершённые конкурсы.'),
@@ -628,8 +628,8 @@ async def finalize_contest_creation(message: types.Message, state: FSMContext, d
await message.answer(texts.t('ADMIN_CONTEST_INVALID_DATE', 'Не удалось распознать дату.')) await message.answer(texts.t('ADMIN_CONTEST_INVALID_DATE', 'Не удалось распознать дату.'))
return return
start_at = datetime.fromisoformat(start_at_raw).astimezone(UTC).replace(tzinfo=None) start_at = datetime.fromisoformat(start_at_raw).astimezone(UTC)
end_at = datetime.fromisoformat(end_at_raw).astimezone(UTC).replace(tzinfo=None) end_at = datetime.fromisoformat(end_at_raw).astimezone(UTC)
contest_type = data.get('contest_type') or 'referral_paid' contest_type = data.get('contest_type') or 'referral_paid'
+10 -10
View File
@@ -1,5 +1,5 @@
import json import json
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
import structlog import structlog
from aiogram import Dispatcher, F, types from aiogram import Dispatcher, F, types
@@ -171,7 +171,7 @@ async def start_round_now(
await db.refresh(tpl) await db.refresh(tpl)
payload = contest_rotation_service._build_payload_for_template(tpl) # type: ignore[attr-defined] payload = contest_rotation_service._build_payload_for_template(tpl) # type: ignore[attr-defined]
now = datetime.utcnow() now = datetime.now(UTC)
ends = now + timedelta(hours=tpl.cooldown_hours) ends = now + timedelta(hours=tpl.cooldown_hours)
await create_round( await create_round(
db, db,
@@ -182,8 +182,8 @@ async def start_round_now(
) )
await contest_rotation_service._announce_round_start( # type: ignore[attr-defined] await contest_rotation_service._announce_round_start( # type: ignore[attr-defined]
tpl, tpl,
now.replace(tzinfo=None), now,
ends.replace(tzinfo=None), ends,
) )
await callback.answer(texts.t('ADMIN_ROUND_STARTED', 'Раунд запущен'), show_alert=True) await callback.answer(texts.t('ADMIN_ROUND_STARTED', 'Раунд запущен'), show_alert=True)
await show_daily_contest(callback, db_user, db) await show_daily_contest(callback, db_user, db)
@@ -214,7 +214,7 @@ async def manual_start_round(
# Для ручного старта не включаем конкурс, если он выключен # Для ручного старта не включаем конкурс, если он выключен
payload = contest_rotation_service._build_payload_for_template(tpl) # type: ignore[attr-defined] payload = contest_rotation_service._build_payload_for_template(tpl) # type: ignore[attr-defined]
now = datetime.utcnow() now = datetime.now(UTC)
ends = now + timedelta(hours=tpl.cooldown_hours) ends = now + timedelta(hours=tpl.cooldown_hours)
await create_round( await create_round(
db, db,
@@ -227,8 +227,8 @@ async def manual_start_round(
# Анонсируем всем пользователям (как тест) # Анонсируем всем пользователям (как тест)
await contest_rotation_service._announce_round_start( # type: ignore[attr-defined] await contest_rotation_service._announce_round_start( # type: ignore[attr-defined]
tpl, tpl,
now.replace(tzinfo=None), now,
ends.replace(tzinfo=None), ends,
) )
await callback.answer(texts.t('ADMIN_ROUND_STARTED', 'Тестовый раунд запущен'), show_alert=True) await callback.answer(texts.t('ADMIN_ROUND_STARTED', 'Тестовый раунд запущен'), show_alert=True)
await show_daily_contest(callback, db_user, db) await show_daily_contest(callback, db_user, db)
@@ -431,7 +431,7 @@ async def start_all_contests(
continue # уже запущен continue # уже запущен
payload = contest_rotation_service._build_payload_for_template(tpl) # type: ignore[attr-defined] payload = contest_rotation_service._build_payload_for_template(tpl) # type: ignore[attr-defined]
now = datetime.utcnow() now = datetime.now(UTC)
ends = now + timedelta(hours=tpl.cooldown_hours) ends = now + timedelta(hours=tpl.cooldown_hours)
await create_round( await create_round(
db, db,
@@ -442,8 +442,8 @@ async def start_all_contests(
) )
await contest_rotation_service._announce_round_start( # type: ignore[attr-defined] await contest_rotation_service._announce_round_start( # type: ignore[attr-defined]
tpl, tpl,
now.replace(tzinfo=None), now,
ends.replace(tzinfo=None), ends,
) )
started_count += 1 started_count += 1
+20 -20
View File
@@ -1,6 +1,6 @@
import asyncio import asyncio
import html import html
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
import structlog import structlog
from aiogram import Dispatcher, F, types from aiogram import Dispatcher, F, types
@@ -148,7 +148,7 @@ async def _persist_broadcast_result(
failed_count: Количество неудачных отправок failed_count: Количество неудачных отправок
status: Финальный статус рассылки ('completed', 'partial', 'failed') status: Финальный статус рассылки ('completed', 'partial', 'failed')
""" """
completed_at = datetime.utcnow() completed_at = datetime.now(UTC)
max_retries = 3 max_retries = 3
retry_delay = 1.0 retry_delay = 1.0
@@ -309,7 +309,7 @@ async def toggle_pinned_message_position(
return return
pinned_message.send_before_menu = not pinned_message.send_before_menu pinned_message.send_before_menu = not pinned_message.send_before_menu
pinned_message.updated_at = datetime.utcnow() pinned_message.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await show_pinned_message_menu(callback, db_user, db, state) await show_pinned_message_menu(callback, db_user, db, state)
@@ -329,7 +329,7 @@ async def toggle_pinned_message_start_mode(
return return
pinned_message.send_on_every_start = not pinned_message.send_on_every_start pinned_message.send_on_every_start = not pinned_message.send_on_every_start
pinned_message.updated_at = datetime.utcnow() pinned_message.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await show_pinned_message_menu(callback, db_user, db, state) await show_pinned_message_menu(callback, db_user, db, state)
@@ -1449,7 +1449,7 @@ async def confirm_broadcast(callback: types.CallbackQuery, db_user: User, state:
async def get_target_users_count(db: AsyncSession, target: str) -> int: async def get_target_users_count(db: AsyncSession, target: str) -> int:
"""Быстрый подсчёт пользователей через SQL COUNT вместо загрузки всех в память.""" """Быстрый подсчёт пользователей через SQL COUNT вместо загрузки всех в память."""
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
from sqlalchemy import distinct, func as sql_func from sqlalchemy import distinct, func as sql_func
@@ -1503,7 +1503,7 @@ async def get_target_users_count(db: AsyncSession, target: str) -> int:
if target == 'expiring': if target == 'expiring':
# Истекающие в ближайшие 3 дня # Истекающие в ближайшие 3 дня
now = datetime.utcnow() now = datetime.now(UTC)
expiry_threshold = now + timedelta(days=3) expiry_threshold = now + timedelta(days=3)
query = ( query = (
select(sql_func.count(distinct(User.id))) select(sql_func.count(distinct(User.id)))
@@ -1520,7 +1520,7 @@ async def get_target_users_count(db: AsyncSession, target: str) -> int:
if target == 'expiring_subscribers': if target == 'expiring_subscribers':
# Истекающие в ближайшие 7 дней # Истекающие в ближайшие 7 дней
now = datetime.utcnow() now = datetime.now(UTC)
expiry_threshold = now + timedelta(days=7) expiry_threshold = now + timedelta(days=7)
query = ( query = (
select(sql_func.count(distinct(User.id))) select(sql_func.count(distinct(User.id)))
@@ -1537,7 +1537,7 @@ async def get_target_users_count(db: AsyncSession, target: str) -> int:
if target == 'expired': if target == 'expired':
# Истекшие подписки # Истекшие подписки
now = datetime.utcnow() now = datetime.now(UTC)
expired_statuses = [SubscriptionStatus.EXPIRED.value, SubscriptionStatus.DISABLED.value] expired_statuses = [SubscriptionStatus.EXPIRED.value, SubscriptionStatus.DISABLED.value]
query = ( query = (
select(sql_func.count(distinct(User.id))) select(sql_func.count(distinct(User.id)))
@@ -1556,7 +1556,7 @@ async def get_target_users_count(db: AsyncSession, target: str) -> int:
if target == 'expired_subscribers': if target == 'expired_subscribers':
# То же что и expired # То же что и expired
now = datetime.utcnow() now = datetime.now(UTC)
expired_statuses = [SubscriptionStatus.EXPIRED.value, SubscriptionStatus.DISABLED.value] expired_statuses = [SubscriptionStatus.EXPIRED.value, SubscriptionStatus.DISABLED.value]
query = ( query = (
select(sql_func.count(distinct(User.id))) select(sql_func.count(distinct(User.id)))
@@ -1634,7 +1634,7 @@ async def get_target_users_count(db: AsyncSession, target: str) -> int:
# Custom filters — быстрый COUNT вместо загрузки всех пользователей # Custom filters — быстрый COUNT вместо загрузки всех пользователей
if target.startswith('custom_'): if target.startswith('custom_'):
now = datetime.utcnow() now = datetime.now(UTC)
today = now.replace(hour=0, minute=0, second=0, microsecond=0) today = now.replace(hour=0, minute=0, second=0, microsecond=0)
criteria = target[len('custom_') :] criteria = target[len('custom_') :]
@@ -1704,7 +1704,7 @@ async def get_target_users(db: AsyncSession, target: str) -> list:
return [sub.user for sub in expiring_subs if sub.user] return [sub.user for sub in expiring_subs if sub.user]
if target == 'expired': if target == 'expired':
now = datetime.utcnow() now = datetime.now(UTC)
expired_statuses = { expired_statuses = {
SubscriptionStatus.EXPIRED.value, SubscriptionStatus.EXPIRED.value,
SubscriptionStatus.DISABLED.value, SubscriptionStatus.DISABLED.value,
@@ -1755,7 +1755,7 @@ async def get_target_users(db: AsyncSession, target: str) -> list:
return [sub.user for sub in expiring_subs if sub.user] return [sub.user for sub in expiring_subs if sub.user]
if target == 'expired_subscribers': if target == 'expired_subscribers':
now = datetime.utcnow() now = datetime.now(UTC)
expired_statuses = { expired_statuses = {
SubscriptionStatus.EXPIRED.value, SubscriptionStatus.EXPIRED.value,
SubscriptionStatus.DISABLED.value, SubscriptionStatus.DISABLED.value,
@@ -1782,7 +1782,7 @@ async def get_target_users(db: AsyncSession, target: str) -> list:
] ]
if target == 'trial_ending': if target == 'trial_ending':
now = datetime.utcnow() now = datetime.now(UTC)
in_3_days = now + timedelta(days=3) in_3_days = now + timedelta(days=3)
return [ return [
user user
@@ -1794,7 +1794,7 @@ async def get_target_users(db: AsyncSession, target: str) -> list:
] ]
if target == 'trial_expired': if target == 'trial_expired':
now = datetime.utcnow() now = datetime.now(UTC)
return [ return [
user user
for user in users for user in users
@@ -1804,7 +1804,7 @@ async def get_target_users(db: AsyncSession, target: str) -> list:
if target == 'autopay_failed': if target == 'autopay_failed':
from app.database.models import SubscriptionEvent from app.database.models import SubscriptionEvent
week_ago = datetime.utcnow() - timedelta(days=7) week_ago = datetime.now(UTC) - timedelta(days=7)
stmt = ( stmt = (
select(SubscriptionEvent.user_id) select(SubscriptionEvent.user_id)
.where( .where(
@@ -1826,15 +1826,15 @@ async def get_target_users(db: AsyncSession, target: str) -> list:
] ]
if target == 'inactive_30d': if target == 'inactive_30d':
threshold = datetime.utcnow() - timedelta(days=30) threshold = datetime.now(UTC) - timedelta(days=30)
return [user for user in users if user.last_activity and user.last_activity < threshold] return [user for user in users if user.last_activity and user.last_activity < threshold]
if target == 'inactive_60d': if target == 'inactive_60d':
threshold = datetime.utcnow() - timedelta(days=60) threshold = datetime.now(UTC) - timedelta(days=60)
return [user for user in users if user.last_activity and user.last_activity < threshold] return [user for user in users if user.last_activity and user.last_activity < threshold]
if target == 'inactive_90d': if target == 'inactive_90d':
threshold = datetime.utcnow() - timedelta(days=90) threshold = datetime.now(UTC) - timedelta(days=90)
return [user for user in users if user.last_activity and user.last_activity < threshold] return [user for user in users if user.last_activity and user.last_activity < threshold]
# Фильтр по тарифу # Фильтр по тарифу
@@ -1855,7 +1855,7 @@ async def get_custom_users_count(db: AsyncSession, criteria: str) -> int:
async def get_custom_users(db: AsyncSession, criteria: str) -> list: async def get_custom_users(db: AsyncSession, criteria: str) -> list:
now = datetime.utcnow() now = datetime.now(UTC)
today = now.replace(hour=0, minute=0, second=0, microsecond=0) today = now.replace(hour=0, minute=0, second=0, microsecond=0)
week_ago = now - timedelta(days=7) week_ago = now - timedelta(days=7)
month_ago = now - timedelta(days=30) month_ago = now - timedelta(days=30)
@@ -1884,7 +1884,7 @@ async def get_custom_users(db: AsyncSession, criteria: str) -> list:
async def get_users_statistics(db: AsyncSession) -> dict: async def get_users_statistics(db: AsyncSession) -> dict:
now = datetime.utcnow() now = datetime.now(UTC)
today = now.replace(hour=0, minute=0, second=0, microsecond=0) today = now.replace(hour=0, minute=0, second=0, microsecond=0)
week_ago = now - timedelta(days=7) week_ago = now - timedelta(days=7)
month_ago = now - timedelta(days=30) month_ago = now - timedelta(days=30)
+13 -10
View File
@@ -139,7 +139,7 @@ def _build_notification_settings_view(language: str):
def _build_notification_preview_message(language: str, notification_type: str): def _build_notification_preview_message(language: str, notification_type: str):
texts = get_texts(language) texts = get_texts(language)
now = datetime.now() now = datetime.now(UTC)
price_30_days = settings.format_price(settings.PRICE_30_DAYS) price_30_days = settings.format_price(settings.PRICE_30_DAYS)
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
@@ -694,7 +694,7 @@ async def force_check_callback(callback: CallbackQuery):
Истекающих подписок: {results['expiring']} Истекающих подписок: {results['expiring']}
Готовых к автооплате: {results['autopay_ready']} Готовых к автооплате: {results['autopay_ready']}
🕐 <b>Время проверки:</b> {datetime.now().strftime('%H:%M:%S')} 🕐 <b>Время проверки:</b> {datetime.now(UTC).strftime('%H:%M:%S')}
Нажмите "Назад" для возврата в меню мониторинга. Нажмите "Назад" для возврата в меню мониторинга.
""" """
@@ -748,7 +748,7 @@ async def traffic_check_callback(callback: CallbackQuery):
Порог дельты: {threshold_gb} ГБ Порог дельты: {threshold_gb} ГБ
Возраст snapshot: {snapshot_age:.1f} мин Возраст snapshot: {snapshot_age:.1f} мин
🕐 <b>Время проверки:</b> {datetime.now().strftime('%H:%M:%S')} 🕐 <b>Время проверки:</b> {datetime.now(UTC).strftime('%H:%M:%S')}
""" """
if violations: if violations:
@@ -862,7 +862,7 @@ async def test_notifications_callback(callback: CallbackQuery):
📊 <b>Статус системы:</b> 📊 <b>Статус системы:</b>
Мониторинг: {'🟢 Работает' if monitoring_service.is_running else '🔴 Остановлен'} Мониторинг: {'🟢 Работает' if monitoring_service.is_running else '🔴 Остановлен'}
Уведомления: {'🟢 Включены' if settings.ENABLE_NOTIFICATIONS else '🔴 Отключены'} Уведомления: {'🟢 Включены' if settings.ENABLE_NOTIFICATIONS else '🔴 Отключены'}
Время теста: {datetime.now().strftime('%H:%M:%S %d.%m.%Y')} Время теста: {datetime.now(UTC).strftime('%H:%M:%S %d.%m.%Y')}
Если вы получили это сообщение, система уведомлений работает корректно! Если вы получили это сообщение, система уведомлений работает корректно!
""" """
@@ -887,7 +887,7 @@ async def monitoring_statistics_callback(callback: CallbackQuery):
mon_status = await monitoring_service.get_monitoring_status(db) mon_status = await monitoring_service.get_monitoring_status(db)
week_ago = datetime.now() - timedelta(days=7) week_ago = datetime.now(UTC) - timedelta(days=7)
week_logs = await monitoring_service.get_monitoring_logs(db, limit=1000) week_logs = await monitoring_service.get_monitoring_logs(db, limit=1000)
week_logs = [log for log in week_logs if log['created_at'] >= week_ago] week_logs = [log for log in week_logs if log['created_at'] >= week_ago]
@@ -1013,7 +1013,7 @@ async def nalogo_force_process_callback(callback: CallbackQuery):
sub_stats = await get_subscriptions_statistics(db) sub_stats = await get_subscriptions_statistics(db)
mon_status = await monitoring_service.get_monitoring_status(db) mon_status = await monitoring_service.get_monitoring_status(db)
week_ago = datetime.now() - timedelta(days=7) week_ago = datetime.now(UTC) - timedelta(days=7)
week_logs = await monitoring_service.get_monitoring_logs(db, limit=1000) week_logs = await monitoring_service.get_monitoring_logs(db, limit=1000)
week_logs = [log for log in week_logs if log['created_at'] >= week_ago] week_logs = [log for log in week_logs if log['created_at'] >= week_ago]
week_success = sum(1 for log in week_logs if log['is_success']) week_success = sum(1 for log in week_logs if log['is_success'])
@@ -1235,7 +1235,7 @@ async def receipts_missing_callback(callback: CallbackQuery):
async def receipts_link_old_callback(callback: CallbackQuery): async def receipts_link_old_callback(callback: CallbackQuery):
"""Привязать старые чеки из NaloGO к транзакциям по сумме и дате.""" """Привязать старые чеки из NaloGO к транзакциям по сумме и дате."""
try: try:
from datetime import date, timedelta from datetime import UTC, date, timedelta
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from sqlalchemy import and_, select from sqlalchemy import and_, select
@@ -1245,7 +1245,7 @@ async def receipts_link_old_callback(callback: CallbackQuery):
await callback.answer('🔄 Загружаю чеки из NaloGO...', show_alert=False) await callback.answer('🔄 Загружаю чеки из NaloGO...', show_alert=False)
TRACKING_START_DATE = datetime(2024, 12, 29, 0, 0, 0) TRACKING_START_DATE = datetime(2024, 12, 29, 0, 0, 0, tzinfo=UTC)
async with AsyncSessionLocal() as db: async with AsyncSessionLocal() as db:
# Получаем старые транзакции без чеков # Получаем старые транзакции без чеков
@@ -1311,9 +1311,12 @@ async def receipts_link_old_callback(callback: CallbackQuery):
try: try:
from dateutil.parser import isoparse from dateutil.parser import isoparse
t.receipt_created_at = isoparse(operation_time) parsed_time = isoparse(operation_time)
t.receipt_created_at = (
parsed_time if parsed_time.tzinfo else parsed_time.replace(tzinfo=UTC)
)
except Exception: except Exception:
t.receipt_created_at = datetime.utcnow() t.receipt_created_at = datetime.now(UTC)
linked += 1 linked += 1
if linked > 0: if linked > 0:
+2 -2
View File
@@ -822,9 +822,9 @@ async def export_payments(
file_bytes = json_content.encode('utf-8') file_bytes = json_content.encode('utf-8')
# Отправляем файл # Отправляем файл
from datetime import datetime from datetime import UTC, datetime
filename = f'payments_export_{datetime.now().strftime("%Y%m%d_%H%M%S")}.json' filename = f'payments_export_{datetime.now(UTC).strftime("%Y%m%d_%H%M%S")}.json'
await callback.message.answer_document( await callback.message.answer_document(
document=BufferedInputFile(file_bytes, filename=filename), document=BufferedInputFile(file_bytes, filename=filename),
+4 -4
View File
@@ -4,7 +4,7 @@ import asyncio
import html import html
import re import re
from collections.abc import Sequence from collections.abc import Sequence
from datetime import datetime from datetime import UTC, datetime
import structlog import structlog
from aiogram import Dispatcher, F, types from aiogram import Dispatcher, F, types
@@ -260,7 +260,7 @@ def _format_offer_remaining(offer, texts) -> str:
if not offer.expires_at: if not offer.expires_at:
return texts.t('ADMIN_PROMO_OFFER_SEND_USER_OFFER_NO_EXPIRY', 'без срока') return texts.t('ADMIN_PROMO_OFFER_SEND_USER_OFFER_NO_EXPIRY', 'без срока')
remaining_seconds = int((offer.expires_at - datetime.utcnow()).total_seconds()) remaining_seconds = int((offer.expires_at - datetime.now(UTC)).total_seconds())
if remaining_seconds <= 0: if remaining_seconds <= 0:
return texts.t('ADMIN_PROMO_OFFER_SEND_USER_OFFER_TIME_LEFT_EXPIRED', 'истекло') return texts.t('ADMIN_PROMO_OFFER_SEND_USER_OFFER_TIME_LEFT_EXPIRED', 'истекло')
@@ -1512,7 +1512,7 @@ async def show_selected_user_details(
lines.append('') lines.append('')
lines.append(texts.t('ADMIN_PROMO_OFFER_SEND_USER_NO_SUBSCRIPTION', '💳 Подписка отсутствует')) lines.append(texts.t('ADMIN_PROMO_OFFER_SEND_USER_NO_SUBSCRIPTION', '💳 Подписка отсутствует'))
now = datetime.utcnow() now = datetime.now(UTC)
percent = 0 percent = 0
try: try:
percent = int(getattr(user, 'promo_offer_discount_percent', 0) or 0) percent = int(getattr(user, 'promo_offer_discount_percent', 0) or 0)
@@ -1772,7 +1772,7 @@ async def show_selected_user_details(
) )
if subscription: if subscription:
now = datetime.utcnow() now = datetime.now(UTC)
result = await db.execute( result = await db.execute(
select(SubscriptionTemporaryAccess) select(SubscriptionTemporaryAccess)
.options(selectinload(SubscriptionTemporaryAccess.offer)) .options(selectinload(SubscriptionTemporaryAccess.offer))
+4 -4
View File
@@ -1,4 +1,4 @@
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
import structlog import structlog
from aiogram import Dispatcher, F, types from aiogram import Dispatcher, F, types
@@ -722,7 +722,7 @@ async def process_promocode_expiry(message: types.Message, db_user: User, state:
valid_until = None valid_until = None
if expiry_days > 0: if expiry_days > 0:
valid_until = datetime.utcnow() + timedelta(days=expiry_days) valid_until = datetime.now(UTC) + timedelta(days=expiry_days)
type_map = { type_map = {
'balance': PromoCodeType.BALANCE, 'balance': PromoCodeType.BALANCE,
@@ -803,7 +803,7 @@ async def process_discount_hours(message: types.Message, db_user: User, state: F
valid_until = None valid_until = None
if expiry_days > 0: if expiry_days > 0:
valid_until = datetime.utcnow() + timedelta(days=expiry_days) valid_until = datetime.now(UTC) + timedelta(days=expiry_days)
# Создаем DISCOUNT промокод # Создаем DISCOUNT промокод
# balance_bonus_kopeks = процент скидки (НЕ копейки!) # balance_bonus_kopeks = процент скидки (НЕ копейки!)
@@ -877,7 +877,7 @@ async def handle_edit_expiry(message: types.Message, db_user: User, state: FSMCo
valid_until = None valid_until = None
if expiry_days > 0: if expiry_days > 0:
valid_until = datetime.utcnow() + timedelta(days=expiry_days) valid_until = datetime.now(UTC) + timedelta(days=expiry_days)
await update_promocode(db, promo, valid_until=valid_until) await update_promocode(db, promo, valid_until=valid_until)
+6 -5
View File
@@ -1,5 +1,6 @@
import datetime import datetime
import json import json
from datetime import UTC
import structlog import structlog
from aiogram import Dispatcher, F, types from aiogram import Dispatcher, F, types
@@ -33,7 +34,7 @@ async def show_referral_statistics(callback: types.CallbackQuery, db_user: User,
if stats.get('active_referrers', 0) > 0: if stats.get('active_referrers', 0) > 0:
avg_per_referrer = stats.get('total_paid_kopeks', 0) / stats['active_referrers'] avg_per_referrer = stats.get('total_paid_kopeks', 0) / stats['active_referrers']
current_time = datetime.datetime.now().strftime('%H:%M:%S') current_time = datetime.datetime.now(UTC).strftime('%H:%M:%S')
text = f""" text = f"""
🤝 <b>Реферальная статистика</b> 🤝 <b>Реферальная статистика</b>
@@ -114,7 +115,7 @@ async def show_referral_statistics(callback: types.CallbackQuery, db_user: User,
except Exception as e: except Exception as e:
logger.error('Ошибка в show_referral_statistics', error=e, exc_info=True) logger.error('Ошибка в show_referral_statistics', error=e, exc_info=True)
current_time = datetime.datetime.now().strftime('%H:%M:%S') current_time = datetime.datetime.now(UTC).strftime('%H:%M:%S')
text = f""" text = f"""
🤝 <b>Реферальная статистика</b> 🤝 <b>Реферальная статистика</b>
@@ -656,7 +657,7 @@ async def process_test_referral_earning(message: types.Message, db_user: User, d
def _get_period_dates(period: str) -> tuple[datetime.datetime, datetime.datetime]: def _get_period_dates(period: str) -> tuple[datetime.datetime, datetime.datetime]:
"""Возвращает начальную и конечную даты для заданного периода.""" """Возвращает начальную и конечную даты для заданного периода."""
now = datetime.datetime.now() now = datetime.datetime.now(UTC)
today = now.replace(hour=0, minute=0, second=0, microsecond=0) today = now.replace(hour=0, minute=0, second=0, microsecond=0)
if period == 'today': if period == 'today':
@@ -1153,9 +1154,9 @@ async def sync_referrals_with_contest(
await callback.answer('🏆 Синхронизирую с конкурсами...') await callback.answer('🏆 Синхронизирую с конкурсами...')
try: try:
from datetime import datetime from datetime import UTC, datetime
now_utc = datetime.utcnow() now_utc = datetime.now(UTC)
# Получаем активные конкурсы # Получаем активные конкурсы
paid_contests = await get_contests_for_events(db, now_utc, contest_types=['referral_paid']) paid_contests = await get_contests_for_events(db, now_utc, contest_types=['referral_paid'])
+4 -4
View File
@@ -1,5 +1,5 @@
import math import math
from datetime import datetime from datetime import UTC, datetime
from typing import Any from typing import Any
import structlog import structlog
@@ -1041,7 +1041,7 @@ async def show_system_stats(callback: types.CallbackQuery, db_user: User, db: As
""" """
text += f""" text += f"""
🕒 <b>Обновлено:</b> {format_datetime(stats.get('last_updated', datetime.now()))} 🕒 <b>Обновлено:</b> {format_datetime(stats.get('last_updated', datetime.now(UTC)))}
""" """
keyboard = [ keyboard = [
@@ -1167,7 +1167,7 @@ async def show_traffic_stats(callback: types.CallbackQuery, db_user: User, db: A
for i, (node_name, total_bytes) in enumerate(sorted_nodes[:5], 1): for i, (node_name, total_bytes) in enumerate(sorted_nodes[:5], 1):
text += f'{i}. {node_name}: {format_bytes(total_bytes)}\n' text += f'{i}. {node_name}: {format_bytes(total_bytes)}\n'
text += f'\n🕒 <b>Обновлено:</b> {format_datetime(datetime.now())}' text += f'\n🕒 <b>Обновлено:</b> {format_datetime(datetime.now(UTC))}'
keyboard = [ keyboard = [
[types.InlineKeyboardButton(text='🔄 Обновить', callback_data='admin_rw_traffic')], [types.InlineKeyboardButton(text='🔄 Обновить', callback_data='admin_rw_traffic')],
@@ -1317,7 +1317,7 @@ async def show_node_statistics(callback: types.CallbackQuery, db_user: User, db:
try: try:
from datetime import datetime, timedelta from datetime import datetime, timedelta
end_date = datetime.now() end_date = datetime.now(UTC)
start_date = end_date - timedelta(days=7) start_date = end_date - timedelta(days=7)
node_usage = await remnawave_service.get_node_user_usage_by_range(node_uuid, start_date, end_date) node_usage = await remnawave_service.get_node_user_usage_by_range(node_uuid, start_date, end_date)
+10 -10
View File
@@ -1,4 +1,4 @@
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
import structlog import structlog
from aiogram import Dispatcher, F, types from aiogram import Dispatcher, F, types
@@ -40,7 +40,7 @@ async def show_users_statistics(callback: types.CallbackQuery, db_user: User, db
total_users = stats['total_users'] total_users = stats['total_users']
active_rate = format_percentage(stats['active_users'] / total_users * 100 if total_users > 0 else 0) active_rate = format_percentage(stats['active_users'] / total_users * 100 if total_users > 0 else 0)
current_time = format_datetime(datetime.utcnow()) current_time = format_datetime(datetime.now(UTC))
text = f""" text = f"""
👥 <b>Статистика пользователей</b> 👥 <b>Статистика пользователей</b>
@@ -89,7 +89,7 @@ async def show_subscriptions_statistics(callback: types.CallbackQuery, db_user:
total_subs = stats['total_subscriptions'] total_subs = stats['total_subscriptions']
conversion_rate = format_percentage(stats['paid_subscriptions'] / total_subs * 100 if total_subs > 0 else 0) conversion_rate = format_percentage(stats['paid_subscriptions'] / total_subs * 100 if total_subs > 0 else 0)
current_time = format_datetime(datetime.utcnow()) current_time = format_datetime(datetime.now(UTC))
text = f""" text = f"""
📱 <b>Статистика подписок</b> 📱 <b>Статистика подписок</b>
@@ -133,12 +133,12 @@ async def show_subscriptions_statistics(callback: types.CallbackQuery, db_user:
@admin_required @admin_required
@error_handler @error_handler
async def show_revenue_statistics(callback: types.CallbackQuery, db_user: User, db: AsyncSession): async def show_revenue_statistics(callback: types.CallbackQuery, db_user: User, db: AsyncSession):
now = datetime.utcnow() now = datetime.now(UTC)
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
month_stats = await get_transactions_statistics(db, month_start, now) month_stats = await get_transactions_statistics(db, month_start, now)
all_time_stats = await get_transactions_statistics(db) all_time_stats = await get_transactions_statistics(db)
current_time = format_datetime(datetime.utcnow()) current_time = format_datetime(datetime.now(UTC))
text = f""" text = f"""
💰 <b>Статистика доходов</b> 💰 <b>Статистика доходов</b>
@@ -189,7 +189,7 @@ async def show_revenue_statistics(callback: types.CallbackQuery, db_user: User,
@error_handler @error_handler
async def show_referral_statistics(callback: types.CallbackQuery, db_user: User, db: AsyncSession): async def show_referral_statistics(callback: types.CallbackQuery, db_user: User, db: AsyncSession):
stats = await get_referral_statistics(db) stats = await get_referral_statistics(db)
current_time = format_datetime(datetime.utcnow()) current_time = format_datetime(datetime.now(UTC))
avg_per_referrer = 0 avg_per_referrer = 0
if stats['active_referrers'] > 0: if stats['active_referrers'] > 0:
@@ -250,10 +250,10 @@ async def show_summary_statistics(callback: types.CallbackQuery, db_user: User,
user_stats = await user_service.get_user_statistics(db) user_stats = await user_service.get_user_statistics(db)
sub_stats = await get_subscriptions_statistics(db) sub_stats = await get_subscriptions_statistics(db)
now = datetime.utcnow() now = datetime.now(UTC)
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
revenue_stats = await get_transactions_statistics(db, month_start, now) revenue_stats = await get_transactions_statistics(db, month_start, now)
current_time = format_datetime(datetime.utcnow()) current_time = format_datetime(datetime.now(UTC))
conversion_rate = 0 conversion_rate = 0
if user_stats['total_users'] > 0: if user_stats['total_users'] > 0:
@@ -317,10 +317,10 @@ async def show_revenue_by_period(callback: types.CallbackQuery, db_user: User, d
revenue_data = await get_revenue_by_period(db, days) revenue_data = await get_revenue_by_period(db, days)
if period == 'yesterday': if period == 'yesterday':
yesterday = datetime.utcnow().date() - timedelta(days=1) yesterday = datetime.now(UTC).date() - timedelta(days=1)
revenue_data = [r for r in revenue_data if r['date'] == yesterday] revenue_data = [r for r in revenue_data if r['date'] == yesterday]
elif period == 'today': elif period == 'today':
today = datetime.utcnow().date() today = datetime.now(UTC).date()
revenue_data = [r for r in revenue_data if r['date'] == today] revenue_data = [r for r in revenue_data if r['date'] == today]
total_revenue = sum(r['amount_kopeks'] for r in revenue_data) total_revenue = sum(r['amount_kopeks'] for r in revenue_data)
+3 -3
View File
@@ -1,4 +1,4 @@
from datetime import datetime from datetime import UTC, datetime
from html import escape from html import escape
from pathlib import Path from pathlib import Path
@@ -47,7 +47,7 @@ def _build_logs_message(log_path: Path) -> str:
total_length = len(content) total_length = len(content)
stats = log_path.stat() stats = log_path.stat()
updated_at = datetime.fromtimestamp(stats.st_mtime) updated_at = datetime.fromtimestamp(stats.st_mtime, tz=UTC)
if not content: if not content:
preview_text = 'Лог-файл пуст.' preview_text = 'Лог-файл пуст.'
@@ -128,7 +128,7 @@ async def download_system_logs(
document = FSInputFile(log_path) document = FSInputFile(log_path)
stats = log_path.stat() stats = log_path.stat()
updated_at = datetime.fromtimestamp(stats.st_mtime).strftime('%d.%m.%Y %H:%M:%S') updated_at = datetime.fromtimestamp(stats.st_mtime, tz=UTC).strftime('%d.%m.%Y %H:%M:%S')
caption = ( caption = (
f'🧾 Лог-файл <code>{log_path.name}</code>\n📁 Путь: <code>{log_path}</code>\n🕒 Обновлен: {updated_at}' f'🧾 Лог-файл <code>{log_path.name}</code>\n📁 Путь: <code>{log_path}</code>\n🕒 Обновлен: {updated_at}'
) )
+3 -3
View File
@@ -1,6 +1,6 @@
import html import html
import time import time
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
import structlog import structlog
from aiogram import Bot, Dispatcher, F, types from aiogram import Bot, Dispatcher, F, types
@@ -449,7 +449,7 @@ async def handle_admin_ticket_reply(message: types.Message, state: FSMContext, d
except ValueError: except ValueError:
await message.answer('❌ Введите целое число минут') await message.answer('❌ Введите целое число минут')
return return
until = datetime.utcnow() + timedelta(minutes=minutes) until = datetime.now(UTC) + timedelta(minutes=minutes)
ok = await TicketCRUD.set_user_reply_block(db, ticket_id, permanent=False, until=until) ok = await TicketCRUD.set_user_reply_block(db, ticket_id, permanent=False, until=until)
if ok: if ok:
await message.answer(f'✅ Пользователь заблокирован на {minutes} минут') await message.answer(f'✅ Пользователь заблокирован на {minutes} минут')
@@ -769,7 +769,7 @@ async def handle_admin_block_duration_input(message: types.Message, state: FSMCo
await state.clear() await state.clear()
return return
until = datetime.utcnow() + timedelta(minutes=minutes) until = datetime.now(UTC) + timedelta(minutes=minutes)
ok = await TicketCRUD.set_user_reply_block(db, ticket_id, permanent=False, until=until) ok = await TicketCRUD.set_user_reply_block(db, ticket_id, permanent=False, until=until)
if not ok: if not ok:
await message.answer('❌ Ошибка блокировки') await message.answer('❌ Ошибка блокировки')
+16 -16
View File
@@ -1,6 +1,6 @@
import re import re
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
from enum import Enum from enum import Enum
from typing import Any from typing import Any
@@ -146,7 +146,7 @@ def _build_user_button_text(
if user.balance_kopeks > 0: if user.balance_kopeks > 0:
button_text += f' | 💰 {settings.format_price(user.balance_kopeks)}' button_text += f' | 💰 {settings.format_price(user.balance_kopeks)}'
if user.subscription and user.subscription.end_date: if user.subscription and user.subscription.end_date:
days_left = (user.subscription.end_date - datetime.utcnow()).days days_left = (user.subscription.end_date - datetime.now(UTC)).days
button_text += f' | 📅 {days_left}д' button_text += f' | 📅 {days_left}д'
elif filter_type == UserFilterType.CAMPAIGN: elif filter_type == UserFilterType.CAMPAIGN:
@@ -425,7 +425,7 @@ async def show_users_ready_to_renew(
text += 'Нажмите на пользователя для управления:' text += 'Нажмите на пользователя для управления:'
keyboard = [] keyboard = []
current_time = datetime.utcnow() current_time = datetime.now(UTC)
for user in users_data['users']: for user in users_data['users']:
subscription = user.subscription subscription = user.subscription
@@ -730,7 +730,7 @@ async def show_users_statistics(callback: types.CallbackQuery, db_user: User, db
from sqlalchemy import func, or_, select from sqlalchemy import func, or_, select
current_time = datetime.utcnow() current_time = datetime.now(UTC)
active_subscription_query = ( active_subscription_query = (
select(func.count(Subscription.id)) select(func.count(Subscription.id))
@@ -859,7 +859,7 @@ async def _render_user_subscription_overview(callback: types.CallbackQuery, db:
text += f'<b>Устройства:</b> {subscription.device_limit}\n' text += f'<b>Устройства:</b> {subscription.device_limit}\n'
if subscription.is_active: if subscription.is_active:
days_left = (subscription.end_date - datetime.utcnow()).days days_left = (subscription.end_date - datetime.now(UTC)).days
text += f'<b>Осталось дней:</b> {days_left}\n' text += f'<b>Осталось дней:</b> {days_left}\n'
current_squads = subscription.connected_squads or [] current_squads = subscription.connected_squads or []
@@ -1558,7 +1558,7 @@ async def _update_referral_commission_percent(
return False, None return False, None
user.referral_commission_percent = percent user.referral_commission_percent = percent
user.updated_at = datetime.utcnow() user.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
@@ -2817,7 +2817,7 @@ async def get_detailed_referral_stats(db: AsyncSession, user_id: int) -> dict:
earnings_by_referral[referral_id] += earning.amount_kopeks earnings_by_referral[referral_id] += earning.amount_kopeks
referrals_detail = [] referrals_detail = []
current_time = datetime.utcnow() current_time = datetime.now(UTC)
for referral in referrals: for referral in referrals:
earned = earnings_by_referral.get(referral.id, 0) earned = earnings_by_referral.get(referral.id, 0)
@@ -3469,7 +3469,7 @@ async def toggle_user_server(callback: types.CallbackQuery, db_user: User, db: A
action_text = 'добавлен' action_text = 'добавлен'
subscription.connected_squads = current_squads subscription.connected_squads = current_squads
subscription.updated_at = datetime.utcnow() subscription.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(subscription) await db.refresh(subscription)
@@ -3872,7 +3872,7 @@ async def _update_user_devices(db: AsyncSession, user_id: int, devices: int, adm
subscription = user.subscription subscription = user.subscription
old_devices = subscription.device_limit old_devices = subscription.device_limit
subscription.device_limit = devices subscription.device_limit = devices
subscription.updated_at = datetime.utcnow() subscription.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
@@ -3916,7 +3916,7 @@ async def _update_user_traffic(db: AsyncSession, user_id: int, traffic_gb: int,
subscription = user.subscription subscription = user.subscription
old_traffic = subscription.traffic_limit_gb old_traffic = subscription.traffic_limit_gb
subscription.traffic_limit_gb = traffic_gb subscription.traffic_limit_gb = traffic_gb
subscription.updated_at = datetime.utcnow() subscription.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
@@ -4038,7 +4038,7 @@ async def _deactivate_user_subscription(db: AsyncSession, user_id: int, admin_id
async def _activate_user_subscription(db: AsyncSession, user_id: int, admin_id: int) -> bool: async def _activate_user_subscription(db: AsyncSession, user_id: int, admin_id: int) -> bool:
try: try:
from datetime import datetime from datetime import UTC, datetime
from app.database.crud.subscription import get_subscription_by_user_id from app.database.crud.subscription import get_subscription_by_user_id
from app.database.models import SubscriptionStatus from app.database.models import SubscriptionStatus
@@ -4050,8 +4050,8 @@ async def _activate_user_subscription(db: AsyncSession, user_id: int, admin_id:
return False return False
subscription.status = SubscriptionStatus.ACTIVE.value subscription.status = SubscriptionStatus.ACTIVE.value
if subscription.end_date <= datetime.utcnow(): if subscription.end_date <= datetime.now(UTC):
subscription.end_date = datetime.utcnow() + timedelta(days=1) subscription.end_date = datetime.now(UTC) + timedelta(days=1)
await db.commit() await db.commit()
await db.refresh(subscription) await db.refresh(subscription)
@@ -4506,7 +4506,7 @@ async def admin_buy_subscription_execute(callback: types.CallbackQuery, db_user:
return return
if subscription: if subscription:
current_time = datetime.utcnow() current_time = datetime.now(UTC)
bonus_period = timedelta() bonus_period = timedelta()
if subscription.is_trial and settings.TRIAL_ADD_REMAINING_DAYS_TO_PAID and subscription.end_date: if subscription.is_trial and settings.TRIAL_ADD_REMAINING_DAYS_TO_PAID and subscription.end_date:
@@ -5102,7 +5102,7 @@ async def _change_subscription_type(db: AsyncSession, user_id: int, new_type: st
new_type_text = 'триальной' if new_is_trial else 'платной' new_type_text = 'триальной' if new_is_trial else 'платной'
subscription.is_trial = new_is_trial subscription.is_trial = new_is_trial
subscription.updated_at = datetime.utcnow() subscription.updated_at = datetime.now(UTC)
if not new_is_trial and subscription.is_trial: if not new_is_trial and subscription.is_trial:
user = await get_user_by_id(db, user_id) user = await get_user_by_id(db, user_id)
@@ -5306,7 +5306,7 @@ async def confirm_admin_tariff_change(callback: types.CallbackQuery, db_user: Us
subscription.device_limit = tariff.device_limit subscription.device_limit = tariff.device_limit
subscription.traffic_limit_gb = tariff.traffic_limit_gb subscription.traffic_limit_gb = tariff.traffic_limit_gb
subscription.connected_squads = tariff.allowed_squads or [] subscription.connected_squads = tariff.allowed_squads or []
subscription.updated_at = datetime.utcnow() subscription.updated_at = datetime.now(UTC)
# Сбрасываем докупленный трафик при смене тарифа # Сбрасываем докупленный трафик при смене тарифа
from sqlalchemy import delete as sql_delete from sqlalchemy import delete as sql_delete
+2 -2
View File
@@ -1,4 +1,4 @@
from datetime import datetime from datetime import UTC, datetime
import structlog import structlog
from aiogram import types from aiogram import types
@@ -251,7 +251,7 @@ async def process_heleket_payment_amount(
await db.execute( await db.execute(
update(payment.__class__) update(payment.__class__)
.where(payment.__class__.id == payment.id) .where(payment.__class__.id == payment.id)
.values(metadata_json=metadata, updated_at=datetime.utcnow()) .values(metadata_json=metadata, updated_at=datetime.now(UTC))
) )
await db.commit() await db.commit()
except Exception as error: # pragma: no cover - diagnostics except Exception as error: # pragma: no cover - diagnostics
+2 -2
View File
@@ -1,5 +1,5 @@
import html import html
from datetime import datetime from datetime import UTC, datetime
from typing import Any from typing import Any
import structlog import structlog
@@ -220,7 +220,7 @@ async def _send_pal24_payment_message(
await db.execute( await db.execute(
update(payment.__class__) update(payment.__class__)
.where(payment.__class__.id == payment.id) .where(payment.__class__.id == payment.id)
.values(metadata_json=metadata, updated_at=datetime.utcnow()) .values(metadata_json=metadata, updated_at=datetime.now(UTC))
) )
await db.commit() await db.commit()
except Exception as error: # pragma: no cover - diagnostics except Exception as error: # pragma: no cover - diagnostics
+2 -2
View File
@@ -1,4 +1,4 @@
from datetime import datetime from datetime import UTC, datetime
import structlog import structlog
from aiogram import types from aiogram import types
@@ -234,7 +234,7 @@ async def process_wata_payment_amount(
await db.execute( await db.execute(
update(payment.__class__) update(payment.__class__)
.where(payment.__class__.id == payment.id) .where(payment.__class__.id == payment.id)
.values(metadata_json=metadata, updated_at=datetime.utcnow()) .values(metadata_json=metadata, updated_at=datetime.now(UTC))
) )
await db.commit() await db.commit()
except Exception as error: # pragma: no cover - diagnostics except Exception as error: # pragma: no cover - diagnostics
+3 -3
View File
@@ -1,4 +1,4 @@
from datetime import datetime from datetime import UTC, datetime
import structlog import structlog
from aiogram import types from aiogram import types
@@ -270,7 +270,7 @@ async def process_yookassa_payment_amount(
await db.execute( await db.execute(
update(payment.__class__) update(payment.__class__)
.where(payment.__class__.id == payment.id) .where(payment.__class__.id == payment.id)
.values(metadata_json=metadata, updated_at=datetime.utcnow()) .values(metadata_json=metadata, updated_at=datetime.now(UTC))
) )
await db.commit() await db.commit()
except Exception as error: # pragma: no cover - диагностический лог except Exception as error: # pragma: no cover - диагностический лог
@@ -509,7 +509,7 @@ async def process_yookassa_sbp_payment_amount(
await db.execute( await db.execute(
update(payment.__class__) update(payment.__class__)
.where(payment.__class__.id == payment.id) .where(payment.__class__.id == payment.id)
.values(metadata_json=metadata, updated_at=datetime.utcnow()) .values(metadata_json=metadata, updated_at=datetime.now(UTC))
) )
await db.commit() await db.commit()
except Exception as error: # pragma: no cover - диагностический лог except Exception as error: # pragma: no cover - диагностический лог
+2 -2
View File
@@ -1,6 +1,6 @@
"""Contest handlers for daily games.""" """Contest handlers for daily games."""
from datetime import datetime from datetime import UTC, datetime
import structlog import structlog
from aiogram import Dispatcher, F, types from aiogram import Dispatcher, F, types
@@ -34,7 +34,7 @@ _attempt_service = ContestAttemptService()
def _check_rate_limit(user_id: int, action: str, limit: int = 1, window_seconds: int = 5) -> bool: def _check_rate_limit(user_id: int, action: str, limit: int = 1, window_seconds: int = 5) -> bool:
"""Check if user exceeds rate limit for contest actions.""" """Check if user exceeds rate limit for contest actions."""
key = f'{user_id}_{action}' key = f'{user_id}_{action}'
now = datetime.utcnow().timestamp() now = datetime.now(UTC).timestamp()
if key not in _rate_limits: if key not in _rate_limits:
_rate_limits[key] = [] _rate_limits[key] = []
+4 -4
View File
@@ -1,5 +1,5 @@
import html import html
from datetime import datetime from datetime import UTC, datetime
from decimal import Decimal from decimal import Decimal
import structlog import structlog
@@ -165,7 +165,7 @@ async def show_main_menu(
texts = get_texts(db_user.language) texts = get_texts(db_user.language)
db_user.last_activity = datetime.utcnow() db_user.last_activity = datetime.now(UTC)
await db.commit() await db.commit()
has_active_subscription = bool(db_user.subscription and db_user.subscription.is_active) has_active_subscription = bool(db_user.subscription and db_user.subscription.is_active)
@@ -1074,7 +1074,7 @@ def _get_subscription_status(user: User, texts, is_daily_tariff: bool = False) -
if not subscription: if not subscription:
return texts.t('SUB_STATUS_NONE', '❌ Отсутствует') return texts.t('SUB_STATUS_NONE', '❌ Отсутствует')
current_time = datetime.utcnow() current_time = datetime.now(UTC)
actual_status = (subscription.actual_status or '').lower() actual_status = (subscription.actual_status or '').lower()
end_date = getattr(subscription, 'end_date', None) end_date = getattr(subscription, 'end_date', None)
end_date_text = format_local_datetime(end_date, '%d.%m.%Y') if end_date else None end_date_text = format_local_datetime(end_date, '%d.%m.%Y') if end_date else None
@@ -1257,7 +1257,7 @@ async def handle_activate_button(callback: types.CallbackQuery, db_user: User, d
subscription = await get_subscription_by_user_id(db, db_user.id) subscription = await get_subscription_by_user_id(db, db_user.id)
# Если подписка активна — ничего не делаем # Если подписка активна — ничего не делаем
if subscription and subscription.status == 'ACTIVE' and subscription.end_date > datetime.utcnow(): if subscription and subscription.status == 'ACTIVE' and subscription.end_date > datetime.now(UTC):
await callback.answer( await callback.answer(
texts.t('SUBSCRIPTION_ALREADY_ACTIVE', '✅ Подписка уже активна!'), texts.t('SUBSCRIPTION_ALREADY_ACTIVE', '✅ Подписка уже активна!'),
show_alert=True, show_alert=True,
+3 -3
View File
@@ -1,5 +1,5 @@
import asyncio import asyncio
from datetime import datetime from datetime import UTC, datetime
import structlog import structlog
from aiogram import Dispatcher, F, types from aiogram import Dispatcher, F, types
@@ -113,7 +113,7 @@ async def handle_poll_start(
return return
if not response.started_at: if not response.started_at:
response.started_at = datetime.utcnow() response.started_at = datetime.now(UTC)
await db.commit() await db.commit()
index, question = await get_next_question(response) index, question = await get_next_question(response)
@@ -213,7 +213,7 @@ async def handle_poll_answer(
await callback.answer() await callback.answer()
return return
response.completed_at = datetime.utcnow() response.completed_at = datetime.now(UTC)
await db.commit() await db.commit()
reward_amount = await reward_user_for_poll(db, response) reward_amount = await reward_user_for_poll(db, response)
+2 -2
View File
@@ -1,4 +1,4 @@
from datetime import datetime from datetime import UTC, datetime
import structlog import structlog
from aiogram import Dispatcher, F, types from aiogram import Dispatcher, F, types
@@ -106,7 +106,7 @@ def _build_status_message(
offline=len(offline_servers), offline=len(offline_servers),
) )
updated_at = datetime.now().strftime('%H:%M:%S') updated_at = datetime.now(UTC).strftime('%H:%M:%S')
lines.extend( lines.extend(
[ [
+2 -2
View File
@@ -1,7 +1,7 @@
"""Обработчики для простой покупки подписки.""" """Обработчики для простой покупки подписки."""
import html import html
from datetime import datetime from datetime import UTC, datetime
from typing import Any from typing import Any
import structlog import structlog
@@ -137,7 +137,7 @@ async def start_simple_subscription_purchase(
elif getattr(current_subscription, 'is_trial', False): elif getattr(current_subscription, 'is_trial', False):
# Это тестовая подписка # Это тестовая подписка
try: try:
days_left = max(0, (current_subscription.end_date - datetime.utcnow()).days) days_left = max(0, (current_subscription.end_date - datetime.now(UTC)).days)
except Exception: except Exception:
days_left = 0 days_left = 0
key = ( key = (
+20 -20
View File
@@ -1,4 +1,4 @@
from datetime import datetime from datetime import UTC, datetime
import structlog import structlog
from aiogram import Bot, Dispatcher, F, types from aiogram import Bot, Dispatcher, F, types
@@ -439,10 +439,10 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession,
logger.info('📝 Фамилия обновлена', old_last_name=old_last_name, last_name=user.last_name) logger.info('📝 Фамилия обновлена', old_last_name=old_last_name, last_name=user.last_name)
profile_updated = True profile_updated = True
user.last_activity = datetime.utcnow() user.last_activity = datetime.now(UTC)
if profile_updated: if profile_updated:
user.updated_at = datetime.utcnow() user.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(user) await db.refresh(user)
logger.info('💾 Профиль пользователя обновлен', telegram_id=user.telegram_id) logger.info('💾 Профиль пользователя обновлен', telegram_id=user.telegram_id)
@@ -589,8 +589,8 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession,
user.username = message.from_user.username user.username = message.from_user.username
user.first_name = message.from_user.first_name user.first_name = message.from_user.first_name
user.last_name = message.from_user.last_name user.last_name = message.from_user.last_name
user.updated_at = datetime.utcnow() user.updated_at = datetime.now(UTC)
user.last_activity = datetime.utcnow() user.last_activity = datetime.now(UTC)
from app.utils.user_utils import generate_unique_referral_code from app.utils.user_utils import generate_unique_referral_code
@@ -1177,10 +1177,10 @@ async def complete_registration_from_callback(callback: types.CallbackQuery, sta
existing_user.balance_kopeks = 0 existing_user.balance_kopeks = 0
existing_user.has_had_paid_subscription = False existing_user.has_had_paid_subscription = False
from datetime import datetime from datetime import UTC, datetime
existing_user.updated_at = datetime.utcnow() existing_user.updated_at = datetime.now(UTC)
existing_user.last_activity = datetime.utcnow() existing_user.last_activity = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(existing_user, ['subscription']) await db.refresh(existing_user, ['subscription'])
@@ -1211,10 +1211,10 @@ async def complete_registration_from_callback(callback: types.CallbackQuery, sta
if referrer_id and not existing_user.referred_by_id: if referrer_id and not existing_user.referred_by_id:
existing_user.referred_by_id = referrer_id existing_user.referred_by_id = referrer_id
from datetime import datetime from datetime import UTC, datetime
existing_user.updated_at = datetime.utcnow() existing_user.updated_at = datetime.now(UTC)
existing_user.last_activity = datetime.utcnow() existing_user.last_activity = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(existing_user, ['subscription']) await db.refresh(existing_user, ['subscription'])
@@ -1443,10 +1443,10 @@ async def complete_registration(message: types.Message, state: FSMContext, db: A
existing_user.balance_kopeks = 0 existing_user.balance_kopeks = 0
existing_user.has_had_paid_subscription = False existing_user.has_had_paid_subscription = False
from datetime import datetime from datetime import UTC, datetime
existing_user.updated_at = datetime.utcnow() existing_user.updated_at = datetime.now(UTC)
existing_user.last_activity = datetime.utcnow() existing_user.last_activity = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(existing_user, ['subscription']) await db.refresh(existing_user, ['subscription'])
@@ -1477,10 +1477,10 @@ async def complete_registration(message: types.Message, state: FSMContext, db: A
if referrer_id and not existing_user.referred_by_id: if referrer_id and not existing_user.referred_by_id:
existing_user.referred_by_id = referrer_id existing_user.referred_by_id = referrer_id
from datetime import datetime from datetime import UTC, datetime
existing_user.updated_at = datetime.utcnow() existing_user.updated_at = datetime.now(UTC)
existing_user.last_activity = datetime.utcnow() existing_user.last_activity = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(existing_user, ['subscription']) await db.refresh(existing_user, ['subscription'])
@@ -1661,11 +1661,11 @@ def _get_subscription_status(user, texts):
subscription = user.subscription subscription = user.subscription
actual_status = getattr(subscription, 'actual_status', None) actual_status = getattr(subscription, 'actual_status', None)
from datetime import datetime from datetime import UTC, datetime
end_date = getattr(subscription, 'end_date', None) end_date = getattr(subscription, 'end_date', None)
end_date_display = format_local_datetime(end_date, '%d.%m.%Y') if end_date else None end_date_display = format_local_datetime(end_date, '%d.%m.%Y') if end_date else None
current_time = datetime.utcnow() current_time = datetime.now(UTC)
if actual_status == 'disabled': if actual_status == 'disabled':
return texts.t('SUB_STATUS_DISABLED', '⚫ Отключена') return texts.t('SUB_STATUS_DISABLED', '⚫ Отключена')
@@ -1927,7 +1927,7 @@ async def required_sub_channel_check(
subscription = user.subscription subscription = user.subscription
if subscription.is_trial and subscription.status == SubscriptionStatus.DISABLED.value: if subscription.is_trial and subscription.status == SubscriptionStatus.DISABLED.value:
subscription.status = SubscriptionStatus.ACTIVE.value subscription.status = SubscriptionStatus.ACTIVE.value
subscription.updated_at = datetime.utcnow() subscription.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(subscription) await db.refresh(subscription)
logger.info( logger.info(
+3 -3
View File
@@ -1,4 +1,4 @@
from datetime import datetime from datetime import UTC, datetime
from aiogram import types from aiogram import types
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
@@ -383,7 +383,7 @@ async def apply_countries_changes(callback: types.CallbackQuery, db_user: User,
) )
subscription.connected_squads = selected_countries subscription.connected_squads = selected_countries
subscription.updated_at = datetime.utcnow() subscription.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
subscription_service = SubscriptionService() subscription_service = SubscriptionService()
@@ -900,7 +900,7 @@ async def confirm_add_countries_to_subscription(
) )
subscription.connected_squads = selected_countries subscription.connected_squads = selected_countries
subscription.updated_at = datetime.utcnow() subscription.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
subscription_service = SubscriptionService() subscription_service = SubscriptionService()
+4 -4
View File
@@ -1,4 +1,4 @@
from datetime import datetime from datetime import UTC, datetime
from aiogram import types from aiogram import types
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -320,7 +320,7 @@ async def confirm_change_devices(callback: types.CallbackQuery, db_user: User, d
if is_daily_tariff: if is_daily_tariff:
# Для суточных тарифов считаем по дням (как в кабинете) # Для суточных тарифов считаем по дням (как в кабинете)
now = datetime.utcnow() now = datetime.now(UTC)
days_left = max(1, (subscription.end_date - now).days) days_left = max(1, (subscription.end_date - now).days)
period_hint_days = days_left period_hint_days = days_left
@@ -555,7 +555,7 @@ async def execute_change_devices(callback: types.CallbackQuery, db_user: User, d
) )
subscription.device_limit = new_devices_count subscription.device_limit = new_devices_count
subscription.updated_at = datetime.utcnow() subscription.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
@@ -1093,7 +1093,7 @@ async def confirm_add_devices(callback: types.CallbackQuery, db_user: User, db:
if is_daily_tariff: if is_daily_tariff:
# Для суточных тарифов считаем по дням (как в кабинете) # Для суточных тарифов считаем по дням (как в кабинете)
now = datetime.utcnow() now = datetime.now(UTC)
days_left = max(1, (subscription.end_date - now).days) days_left = max(1, (subscription.end_date - now).days)
period_hint_days = days_left period_hint_days = days_left
+2 -2
View File
@@ -446,13 +446,13 @@ async def get_subscription_info_text(subscription, texts, db_user, db: AsyncSess
# Отображаем докупленный трафик # Отображаем докупленный трафик
if (subscription.traffic_limit_gb or 0) > 0: # Только для лимитированных тарифов if (subscription.traffic_limit_gb or 0) > 0: # Только для лимитированных тарифов
from datetime import datetime from datetime import UTC, datetime
from sqlalchemy import select as sql_select from sqlalchemy import select as sql_select
from app.database.models import TrafficPurchase from app.database.models import TrafficPurchase
now = datetime.utcnow() now = datetime.now(UTC)
purchases_query = ( purchases_query = (
sql_select(TrafficPurchase) sql_select(TrafficPurchase)
.where(TrafficPurchase.subscription_id == subscription.id) .where(TrafficPurchase.subscription_id == subscription.id)
+2 -2
View File
@@ -1,4 +1,4 @@
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
from typing import Any from typing import Any
from aiogram import types from aiogram import types
@@ -126,7 +126,7 @@ async def claim_discount_offer(
) )
return return
now = datetime.utcnow() now = datetime.now(UTC)
if offer.claimed_at is not None: if offer.claimed_at is not None:
await callback.answer( await callback.answer(
texts.get('DISCOUNT_CLAIM_ALREADY', 'ℹ️ Скидка уже была активирована'), texts.get('DISCOUNT_CLAIM_ALREADY', 'ℹ️ Скидка уже была активирована'),
+11 -11
View File
@@ -1,4 +1,4 @@
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
from typing import Any from typing import Any
import structlog import structlog
@@ -206,7 +206,7 @@ async def show_subscription_info(callback: types.CallbackQuery, db_user: User, d
await db.refresh(subscription) await db.refresh(subscription)
await db.refresh(db_user) await db.refresh(db_user)
current_time = datetime.utcnow() current_time = datetime.now(UTC)
if subscription.status == 'expired' or subscription.end_date <= current_time: if subscription.status == 'expired' or subscription.end_date <= current_time:
actual_status = 'expired' actual_status = 'expired'
@@ -348,10 +348,10 @@ async def show_subscription_info(callback: types.CallbackQuery, db_user: User, d
tariff_info_lines.append('⏸️ <b>Подписка приостановлена</b>') tariff_info_lines.append('⏸️ <b>Подписка приостановлена</b>')
# Показываем оставшееся время даже при паузе # Показываем оставшееся время даже при паузе
if last_charge: if last_charge:
from datetime import timedelta from datetime import UTC, timedelta
next_charge = last_charge + timedelta(hours=24) next_charge = last_charge + timedelta(hours=24)
now = datetime.utcnow() now = datetime.now(UTC)
if next_charge > now: if next_charge > now:
time_until = next_charge - now time_until = next_charge - now
hours_left = time_until.seconds // 3600 hours_left = time_until.seconds // 3600
@@ -359,10 +359,10 @@ async def show_subscription_info(callback: types.CallbackQuery, db_user: User, d
tariff_info_lines.append(f'⏳ Осталось: {hours_left}ч {minutes_left}мин') tariff_info_lines.append(f'⏳ Осталось: {hours_left}ч {minutes_left}мин')
tariff_info_lines.append('💤 Списание приостановлено') tariff_info_lines.append('💤 Списание приостановлено')
elif last_charge: elif last_charge:
from datetime import timedelta from datetime import UTC, timedelta
next_charge = last_charge + timedelta(hours=24) next_charge = last_charge + timedelta(hours=24)
now = datetime.utcnow() now = datetime.now(UTC)
if next_charge > now: if next_charge > now:
time_until = next_charge - now time_until = next_charge - now
@@ -470,7 +470,7 @@ async def show_subscription_info(callback: types.CallbackQuery, db_user: User, d
from app.database.models import TrafficPurchase from app.database.models import TrafficPurchase
now = datetime.utcnow() now = datetime.now(UTC)
purchases_query = ( purchases_query = (
sql_select(TrafficPurchase) sql_select(TrafficPurchase)
.where(TrafficPurchase.subscription_id == subscription.id) .where(TrafficPurchase.subscription_id == subscription.id)
@@ -1970,7 +1970,7 @@ async def confirm_extend_subscription(callback: types.CallbackQuery, db_user: Us
await callback.answer('⚠ Ошибка списания средств', show_alert=True) await callback.answer('⚠ Ошибка списания средств', show_alert=True)
return return
current_time = datetime.utcnow() current_time = datetime.now(UTC)
if subscription.end_date > current_time: if subscription.end_date > current_time:
new_end_date = subscription.end_date + timedelta(days=days) new_end_date = subscription.end_date + timedelta(days=days)
@@ -2589,7 +2589,7 @@ async def confirm_purchase(callback: types.CallbackQuery, state: FSMContext, db_
should_update_devices = selected_devices is not None should_update_devices = selected_devices is not None
was_trial_conversion = False was_trial_conversion = False
current_time = datetime.utcnow() current_time = datetime.now(UTC)
if existing_subscription: if existing_subscription:
logger.info('Обновляем существующую подписку пользователя', telegram_id=db_user.telegram_id) logger.info('Обновляем существующую подписку пользователя', telegram_id=db_user.telegram_id)
@@ -4311,7 +4311,7 @@ async def _extend_existing_subscription(
squad_uuid: str, squad_uuid: str,
): ):
"""Продлевает существующую подписку.""" """Продлевает существующую подписку."""
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
from app.database.crud.transaction import create_transaction from app.database.crud.transaction import create_transaction
from app.database.crud.user import subtract_user_balance from app.database.crud.user import subtract_user_balance
@@ -4408,7 +4408,7 @@ async def _extend_existing_subscription(
return return
# Обновляем параметры подписки # Обновляем параметры подписки
current_time = datetime.utcnow() current_time = datetime.now(UTC)
old_end_date = current_subscription.end_date old_end_date = current_subscription.end_date
# Обновляем параметры в зависимости от типа текущей подписки # Обновляем параметры в зависимости от типа текущей подписки
+18 -18
View File
@@ -1251,7 +1251,7 @@ async def confirm_daily_tariff_purchase(
state: FSMContext, state: FSMContext,
): ):
"""Подтверждает покупку суточного тарифа.""" """Подтверждает покупку суточного тарифа."""
from datetime import datetime from datetime import UTC, datetime
tariff_id = int(callback.data.split(':')[1]) tariff_id = int(callback.data.split(':')[1])
tariff = await get_tariff_by_id(db, tariff_id) tariff = await get_tariff_by_id(db, tariff_id)
@@ -1314,9 +1314,9 @@ async def confirm_daily_tariff_purchase(
existing_subscription.status = 'active' existing_subscription.status = 'active'
existing_subscription.is_trial = False # Сбрасываем триальный статус existing_subscription.is_trial = False # Сбрасываем триальный статус
existing_subscription.is_daily_paused = False existing_subscription.is_daily_paused = False
existing_subscription.last_daily_charge_at = datetime.utcnow() existing_subscription.last_daily_charge_at = datetime.now(UTC)
# Для суточного тарифа ставим срок на 1 день # Для суточного тарифа ставим срок на 1 день
existing_subscription.end_date = datetime.utcnow() + timedelta(days=1) existing_subscription.end_date = datetime.now(UTC) + timedelta(days=1)
# Сбрасываем докупленный трафик при смене тарифа # Сбрасываем докупленный трафик при смене тарифа
from sqlalchemy import delete as sql_delete from sqlalchemy import delete as sql_delete
@@ -1344,7 +1344,7 @@ async def confirm_daily_tariff_purchase(
tariff_id=tariff.id, tariff_id=tariff.id,
) )
# Устанавливаем время последнего списания # Устанавливаем время последнего списания
subscription.last_daily_charge_at = datetime.utcnow() subscription.last_daily_charge_at = datetime.now(UTC)
subscription.is_daily_paused = False subscription.is_daily_paused = False
await db.commit() await db.commit()
await db.refresh(subscription) await db.refresh(subscription)
@@ -2022,9 +2022,9 @@ async def select_tariff_switch(
current_subscription = await get_subscription_by_user_id(db, db_user.id) current_subscription = await get_subscription_by_user_id(db, db_user.id)
days_warning = '' days_warning = ''
if current_subscription and current_subscription.end_date: if current_subscription and current_subscription.end_date:
from datetime import datetime from datetime import UTC, datetime
remaining = current_subscription.end_date - datetime.utcnow() remaining = current_subscription.end_date - datetime.now(UTC)
remaining_days = max(0, remaining.days) remaining_days = max(0, remaining.days)
if remaining_days > 1: if remaining_days > 1:
days_warning = f'\n\n⚠️ <b>Внимание!</b> У вас осталось {remaining_days} дн. подписки.\nПри смене на суточный тариф они будут утеряны!' days_warning = f'\n\n⚠️ <b>Внимание!</b> У вас осталось {remaining_days} дн. подписки.\nПри смене на суточный тариф они будут утеряны!'
@@ -2103,7 +2103,7 @@ async def select_tariff_switch_period(
state: FSMContext, state: FSMContext,
): ):
"""Обрабатывает выбор периода для переключения тарифа.""" """Обрабатывает выбор периода для переключения тарифа."""
from datetime import datetime from datetime import UTC, datetime
parts = callback.data.split(':') parts = callback.data.split(':')
tariff_id = int(parts[1]) tariff_id = int(parts[1])
@@ -2140,7 +2140,7 @@ async def select_tariff_switch_period(
# Получаем текущую подписку для расчёта оставшегося времени # Получаем текущую подписку для расчёта оставшегося времени
subscription = await get_subscription_by_user_id(db, db_user.id) subscription = await get_subscription_by_user_id(db, db_user.id)
if subscription and subscription.end_date: if subscription and subscription.end_date:
max(0, (subscription.end_date - datetime.utcnow()).days) max(0, (subscription.end_date - datetime.now(UTC)).days)
# При смене тарифа устанавливается ровно оплаченный период # При смене тарифа устанавливается ровно оплаченный период
time_info = f'⏰ Будет установлено: {period} дней' time_info = f'⏰ Будет установлено: {period} дней'
@@ -2361,7 +2361,7 @@ async def confirm_daily_tariff_switch(
state: FSMContext, state: FSMContext,
): ):
"""Подтверждает смену на суточный тариф.""" """Подтверждает смену на суточный тариф."""
from datetime import datetime from datetime import UTC, datetime
tariff_id = int(callback.data.split(':')[1]) tariff_id = int(callback.data.split(':')[1])
tariff = await get_tariff_by_id(db, tariff_id) tariff = await get_tariff_by_id(db, tariff_id)
@@ -2426,9 +2426,9 @@ async def confirm_daily_tariff_switch(
subscription.status = 'active' subscription.status = 'active'
subscription.is_trial = False # Сбрасываем триальный статус subscription.is_trial = False # Сбрасываем триальный статус
subscription.is_daily_paused = False subscription.is_daily_paused = False
subscription.last_daily_charge_at = datetime.utcnow() subscription.last_daily_charge_at = datetime.now(UTC)
# Для суточного тарифа ставим срок на 1 день # Для суточного тарифа ставим срок на 1 день
subscription.end_date = datetime.utcnow() + timedelta(days=1) subscription.end_date = datetime.now(UTC) + timedelta(days=1)
# Сбрасываем докупленный трафик при смене тарифа # Сбрасываем докупленный трафик при смене тарифа
from sqlalchemy import delete as sql_delete from sqlalchemy import delete as sql_delete
@@ -2686,7 +2686,7 @@ async def show_instant_switch_list(
state: FSMContext, state: FSMContext,
): ):
"""Показывает список тарифов для мгновенного переключения.""" """Показывает список тарифов для мгновенного переключения."""
from datetime import datetime from datetime import UTC, datetime
texts = get_texts(db_user.language) texts = get_texts(db_user.language)
await state.clear() await state.clear()
@@ -2710,7 +2710,7 @@ async def show_instant_switch_list(
# Рассчитываем оставшиеся дни # Рассчитываем оставшиеся дни
remaining_days = 0 remaining_days = 0
if subscription.end_date: if subscription.end_date:
remaining_days = max(0, (subscription.end_date - datetime.utcnow()).days) remaining_days = max(0, (subscription.end_date - datetime.now(UTC)).days)
if remaining_days == 0: if remaining_days == 0:
await callback.message.edit_text( await callback.message.edit_text(
@@ -2767,7 +2767,7 @@ async def preview_instant_switch(
state: FSMContext, state: FSMContext,
): ):
"""Показывает превью мгновенного переключения тарифа.""" """Показывает превью мгновенного переключения тарифа."""
from datetime import datetime from datetime import UTC, datetime
tariff_id = int(callback.data.split(':')[1]) tariff_id = int(callback.data.split(':')[1])
new_tariff = await get_tariff_by_id(db, tariff_id) new_tariff = await get_tariff_by_id(db, tariff_id)
@@ -2794,7 +2794,7 @@ async def preview_instant_switch(
return return
if not remaining_days and subscription.end_date: if not remaining_days and subscription.end_date:
remaining_days = max(0, (subscription.end_date - datetime.utcnow()).days) remaining_days = max(0, (subscription.end_date - datetime.now(UTC)).days)
# Рассчитываем стоимость переключения # Рассчитываем стоимость переключения
upgrade_cost, is_upgrade = _calculate_instant_switch_cost(current_tariff, new_tariff, remaining_days, db_user) upgrade_cost, is_upgrade = _calculate_instant_switch_cost(current_tariff, new_tariff, remaining_days, db_user)
@@ -2925,7 +2925,7 @@ async def confirm_instant_switch(
state: FSMContext, state: FSMContext,
): ):
"""Подтверждает мгновенное переключение тарифа.""" """Подтверждает мгновенное переключение тарифа."""
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
tariff_id = int(callback.data.split(':')[1]) tariff_id = int(callback.data.split(':')[1])
new_tariff = await get_tariff_by_id(db, tariff_id) new_tariff = await get_tariff_by_id(db, tariff_id)
@@ -3013,10 +3013,10 @@ async def confirm_instant_switch(
description=f'Переключение на суточный тариф {new_tariff.name} (первый день)', description=f'Переключение на суточный тариф {new_tariff.name} (первый день)',
) )
subscription.end_date = datetime.utcnow() + timedelta(days=1) subscription.end_date = datetime.now(UTC) + timedelta(days=1)
subscription.is_trial = False subscription.is_trial = False
subscription.is_daily_paused = False subscription.is_daily_paused = False
subscription.last_daily_charge_at = datetime.utcnow() subscription.last_daily_charge_at = datetime.now(UTC)
await db.commit() await db.commit()
await db.refresh(subscription) await db.refresh(subscription)
+3 -3
View File
@@ -1,4 +1,4 @@
from datetime import datetime from datetime import UTC, datetime
from aiogram import types from aiogram import types
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
@@ -303,7 +303,7 @@ async def confirm_reset_traffic(callback: types.CallbackQuery, db_user: User, db
return return
subscription.traffic_used_gb = 0.0 subscription.traffic_used_gb = 0.0
subscription.updated_at = datetime.utcnow() subscription.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
SubscriptionService() SubscriptionService()
@@ -826,7 +826,7 @@ async def execute_switch_traffic(callback: types.CallbackQuery, db_user: User, d
await db.execute(delete(TrafficPurchase).where(TrafficPurchase.subscription_id == subscription.id)) await db.execute(delete(TrafficPurchase).where(TrafficPurchase.subscription_id == subscription.id))
subscription.purchased_traffic_gb = 0 subscription.purchased_traffic_gb = 0
subscription.traffic_reset_at = None # Сбрасываем дату сброса трафика subscription.traffic_reset_at = None # Сбрасываем дату сброса трафика
subscription.updated_at = datetime.utcnow() subscription.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
+4 -4
View File
@@ -68,7 +68,7 @@ async def get_main_menu_keyboard_async(
subscription_days_left = subscription.days_left subscription_days_left = subscription.days_left
elif hasattr(subscription, 'end_date') and subscription.end_date: elif hasattr(subscription, 'end_date') and subscription.end_date:
# Fallback: вычисляем вручную, используя UTC # Fallback: вычисляем вручную, используя UTC
now_utc = datetime.now(UTC).replace(tzinfo=None) now_utc = datetime.now(UTC)
days_left = (subscription.end_date - now_utc).days days_left = (subscription.end_date - now_utc).days
subscription_days_left = max(0, days_left) subscription_days_left = max(0, days_left)
@@ -93,7 +93,7 @@ async def get_main_menu_keyboard_async(
# Дни с регистрации # Дни с регистрации
if hasattr(user, 'created_at') and user.created_at: if hasattr(user, 'created_at') and user.created_at:
now_utc = datetime.now(UTC).replace(tzinfo=None) now_utc = datetime.now(UTC)
registration_days = (now_utc - user.created_at).days registration_days = (now_utc - user.created_at).days
# ID промо-группы # ID промо-группы
@@ -2033,9 +2033,9 @@ def get_change_devices_keyboard(
# Для суточных тарифов считаем по дням, для обычных - по месяцам # Для суточных тарифов считаем по дням, для обычных - по месяцам
if is_daily_tariff and subscription_end_date: if is_daily_tariff and subscription_end_date:
# Суточный тариф: цена за оставшиеся дни (обычно 1 день) # Суточный тариф: цена за оставшиеся дни (обычно 1 день)
from datetime import datetime from datetime import UTC, datetime
now = datetime.utcnow() now = datetime.now(UTC)
days_left = max(1, (subscription_end_date - now).days) days_left = max(1, (subscription_end_date - now).days)
# Множитель = days_left / 30 (как в кабинете) # Множитель = days_left / 30 (как в кабинете)
price_multiplier = days_left / 30 price_multiplier = days_left / 30
+3 -3
View File
@@ -1,6 +1,6 @@
import asyncio import asyncio
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
from datetime import datetime from datetime import UTC, datetime
from typing import Any from typing import Any
import structlog import structlog
@@ -194,10 +194,10 @@ class AuthMiddleware(BaseMiddleware):
) )
profile_updated = True profile_updated = True
db_user.last_activity = datetime.utcnow() db_user.last_activity = datetime.now(UTC)
if profile_updated: if profile_updated:
db_user.updated_at = datetime.utcnow() db_user.updated_at = datetime.now(UTC)
logger.info('💾 [Middleware] Профиль пользователя обновлен в middleware', user_id=user.id) logger.info('💾 [Middleware] Профиль пользователя обновлен в middleware', user_id=user.id)
if db_user.remnawave_uuid: if db_user.remnawave_uuid:
+2 -2
View File
@@ -1,5 +1,5 @@
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
from datetime import datetime from datetime import UTC, datetime
from typing import Any from typing import Any
import redis.asyncio as aioredis import redis.asyncio as aioredis
@@ -404,7 +404,7 @@ class ChannelCheckerMiddleware(BaseMiddleware):
return return
# Проверяем что подписка ещё не истекла # Проверяем что подписка ещё не истекла
if subscription.end_date and subscription.end_date <= datetime.utcnow(): if subscription.end_date and subscription.end_date <= datetime.now(UTC):
return return
# Реактивируем в БД # Реактивируем в БД
+2 -2
View File
@@ -1,5 +1,5 @@
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
from typing import Any from typing import Any
import structlog import structlog
@@ -37,7 +37,7 @@ class SubscriptionStatusMiddleware(BaseMiddleware):
if db and user and user.subscription: if db and user and user.subscription:
try: try:
current_time = datetime.utcnow() current_time = datetime.now(UTC)
subscription = user.subscription subscription = user.subscription
if ( if (
+22 -22
View File
@@ -1,4 +1,4 @@
from datetime import datetime from datetime import UTC, datetime
from typing import Any from typing import Any
import structlog import structlog
@@ -275,7 +275,7 @@ class AdminNotificationService:
transaction=None, transaction=None,
amount_kopeks=charged_amount_kopeks, amount_kopeks=charged_amount_kopeks,
message='Trial activation', message='Trial activation',
occurred_at=datetime.utcnow(), occurred_at=datetime.now(UTC),
extra={ extra={
'charged_amount_kopeks': charged_amount_kopeks, 'charged_amount_kopeks': charged_amount_kopeks,
'trial_duration_days': settings.TRIAL_DURATION_DAYS, 'trial_duration_days': settings.TRIAL_DURATION_DAYS,
@@ -356,7 +356,7 @@ class AdminNotificationService:
message_lines.append(f'🔗 <b>Реферер:</b> {referrer_info}') message_lines.append(f'🔗 <b>Реферер:</b> {referrer_info}')
message_lines.append('') message_lines.append('')
message_lines.append(f'⏰ <i>{format_local_datetime(datetime.utcnow(), "%d.%m.%Y %H:%M:%S")}</i>') message_lines.append(f'⏰ <i>{format_local_datetime(datetime.now(UTC), "%d.%m.%Y %H:%M:%S")}</i>')
return await self._send_message('\n'.join(message_lines)) return await self._send_message('\n'.join(message_lines))
@@ -403,7 +403,7 @@ class AdminNotificationService:
transaction=transaction, transaction=transaction,
amount_kopeks=total_amount, amount_kopeks=total_amount,
message='Subscription purchase', message='Subscription purchase',
occurred_at=(transaction.completed_at or transaction.created_at) if transaction else datetime.utcnow(), occurred_at=(transaction.completed_at or transaction.created_at) if transaction else datetime.now(UTC),
extra={ extra={
'period_days': period_days, 'period_days': period_days,
'was_trial_conversion': was_trial_conversion, 'was_trial_conversion': was_trial_conversion,
@@ -482,7 +482,7 @@ class AdminNotificationService:
message_lines.extend( message_lines.extend(
[ [
'', '',
f'<i>{format_local_datetime(datetime.utcnow(), "%d.%m.%Y %H:%M")}</i>', f'<i>{format_local_datetime(datetime.now(UTC), "%d.%m.%Y %H:%M")}</i>',
] ]
) )
@@ -503,7 +503,7 @@ class AdminNotificationService:
repo = getattr(settings, 'VERSION_CHECK_REPO', 'fr1ngg/remnawave-bedolaga-telegram-bot') repo = getattr(settings, 'VERSION_CHECK_REPO', 'fr1ngg/remnawave-bedolaga-telegram-bot')
release_url = f'https://github.com/{repo}/releases/tag/{latest_version.tag_name}' release_url = f'https://github.com/{repo}/releases/tag/{latest_version.tag_name}'
repo_url = f'https://github.com/{repo}' repo_url = f'https://github.com/{repo}'
timestamp = format_local_datetime(datetime.utcnow(), '%d.%m.%Y %H:%M:%S') timestamp = format_local_datetime(datetime.now(UTC), '%d.%m.%Y %H:%M:%S')
if latest_version.prerelease: if latest_version.prerelease:
header = '🧪 <b>Pre-release</b>' header = '🧪 <b>Pre-release</b>'
@@ -568,7 +568,7 @@ class AdminNotificationService:
🔄 Следующая попытка через час. 🔄 Следующая попытка через час.
Проверьте доступность GitHub API и настройки сети. Проверьте доступность GitHub API и настройки сети.
<i>Система автоматических обновлений {format_local_datetime(datetime.utcnow(), '%d.%m.%Y %H:%M:%S')}</i>""" <i>Система автоматических обновлений {format_local_datetime(datetime.now(UTC), '%d.%m.%Y %H:%M:%S')}</i>"""
return await self._send_message(message) return await self._send_message(message)
@@ -590,7 +590,7 @@ class AdminNotificationService:
payment_method = self._get_payment_method_display(transaction.payment_method) payment_method = self._get_payment_method_display(transaction.payment_method)
balance_change = user.balance_kopeks - old_balance balance_change = user.balance_kopeks - old_balance
subscription_status = self._get_subscription_status(subscription) subscription_status = self._get_subscription_status(subscription)
timestamp = format_local_datetime(datetime.utcnow(), '%d.%m.%Y %H:%M:%S') timestamp = format_local_datetime(datetime.now(UTC), '%d.%m.%Y %H:%M:%S')
user_display = self._get_user_display(user) user_display = self._get_user_display(user)
user_id_display = self._get_user_identifier_display(user) user_id_display = self._get_user_identifier_display(user)
@@ -883,7 +883,7 @@ class AdminNotificationService:
💰 <b>Баланс после операции:</b> {settings.format_price(current_balance)} 💰 <b>Баланс после операции:</b> {settings.format_price(current_balance)}
<i>{format_local_datetime(datetime.utcnow(), '%d.%m.%Y %H:%M:%S')}</i>""" <i>{format_local_datetime(datetime.now(UTC), '%d.%m.%Y %H:%M:%S')}</i>"""
return await self._send_message(message) return await self._send_message(message)
@@ -909,7 +909,7 @@ class AdminNotificationService:
transaction=None, transaction=None,
amount_kopeks=promocode_data.get('balance_bonus_kopeks'), amount_kopeks=promocode_data.get('balance_bonus_kopeks'),
message='Promocode activation', message='Promocode activation',
occurred_at=datetime.utcnow(), occurred_at=datetime.now(UTC),
extra={ extra={
'code': promocode_data.get('code'), 'code': promocode_data.get('code'),
'type': promocode_data.get('type'), 'type': promocode_data.get('type'),
@@ -988,7 +988,7 @@ class AdminNotificationService:
'📝 <b>Эффект:</b>', '📝 <b>Эффект:</b>',
effect_description.strip() or '✅ Промокод активирован', effect_description.strip() or '✅ Промокод активирован',
'', '',
f'⏰ <i>{format_local_datetime(datetime.utcnow(), "%d.%m.%Y %H:%M:%S")}</i>', f'⏰ <i>{format_local_datetime(datetime.now(UTC), "%d.%m.%Y %H:%M:%S")}</i>',
] ]
) )
@@ -1015,7 +1015,7 @@ class AdminNotificationService:
transaction=None, transaction=None,
amount_kopeks=None, amount_kopeks=None,
message='Referral link visit', message='Referral link visit',
occurred_at=datetime.utcnow(), occurred_at=datetime.now(UTC),
extra={ extra={
'campaign_id': campaign.id, 'campaign_id': campaign.id,
'campaign_name': campaign.name, 'campaign_name': campaign.name,
@@ -1067,7 +1067,7 @@ class AdminNotificationService:
message_lines.extend( message_lines.extend(
[ [
'', '',
f'⏰ <i>{format_local_datetime(datetime.utcnow(), "%d.%m.%Y %H:%M:%S")}</i>', f'⏰ <i>{format_local_datetime(datetime.now(UTC), "%d.%m.%Y %H:%M:%S")}</i>',
] ]
) )
@@ -1096,7 +1096,7 @@ class AdminNotificationService:
subscription=None, subscription=None,
transaction=None, transaction=None,
message='Promo group change', message='Promo group change',
occurred_at=datetime.utcnow(), occurred_at=datetime.now(UTC),
extra={ extra={
'old_group_id': getattr(old_group, 'id', None), 'old_group_id': getattr(old_group, 'id', None),
'old_group_name': getattr(old_group, 'name', None), 'old_group_name': getattr(old_group, 'name', None),
@@ -1157,7 +1157,7 @@ class AdminNotificationService:
[ [
'', '',
f'💰 Баланс пользователя: {settings.format_price(user.balance_kopeks)}', f'💰 Баланс пользователя: {settings.format_price(user.balance_kopeks)}',
f'⏰ <i>{format_local_datetime(datetime.utcnow(), "%d.%m.%Y %H:%M:%S")}</i>', f'⏰ <i>{format_local_datetime(datetime.now(UTC), "%d.%m.%Y %H:%M:%S")}</i>',
] ]
) )
@@ -1325,7 +1325,7 @@ class AdminNotificationService:
if details.get('enabled_at'): if details.get('enabled_at'):
enabled_at = details['enabled_at'] enabled_at = details['enabled_at']
if isinstance(enabled_at, str): if isinstance(enabled_at, str):
from datetime import datetime from datetime import UTC, datetime
enabled_at = datetime.fromisoformat(enabled_at) enabled_at = datetime.fromisoformat(enabled_at)
message_parts.append( message_parts.append(
@@ -1342,7 +1342,7 @@ class AdminNotificationService:
if details.get('disabled_at'): if details.get('disabled_at'):
disabled_at = details['disabled_at'] disabled_at = details['disabled_at']
if isinstance(disabled_at, str): if isinstance(disabled_at, str):
from datetime import datetime from datetime import UTC, datetime
disabled_at = datetime.fromisoformat(disabled_at) disabled_at = datetime.fromisoformat(disabled_at)
message_parts.append( message_parts.append(
@@ -1409,7 +1409,7 @@ class AdminNotificationService:
message_parts.append('Автоматический мониторинг API остановлен.') message_parts.append('Автоматический мониторинг API остановлен.')
message_parts.append('') message_parts.append('')
message_parts.append(f'⏰ <i>{format_local_datetime(datetime.utcnow(), "%d.%m.%Y %H:%M:%S")}</i>') message_parts.append(f'⏰ <i>{format_local_datetime(datetime.now(UTC), "%d.%m.%Y %H:%M:%S")}</i>')
message = '\n'.join(message_parts) message = '\n'.join(message_parts)
@@ -1446,7 +1446,7 @@ class AdminNotificationService:
if details.get('last_check'): if details.get('last_check'):
last_check = details['last_check'] last_check = details['last_check']
if isinstance(last_check, str): if isinstance(last_check, str):
from datetime import datetime from datetime import UTC, datetime
last_check = datetime.fromisoformat(last_check) last_check = datetime.fromisoformat(last_check)
message_parts.append(f'🕐 <b>Последняя проверка:</b> {format_local_datetime(last_check, "%H:%M:%S")}') message_parts.append(f'🕐 <b>Последняя проверка:</b> {format_local_datetime(last_check, "%H:%M:%S")}')
@@ -1496,7 +1496,7 @@ class AdminNotificationService:
message_parts.append('Панель временно недоступна для обслуживания.') message_parts.append('Панель временно недоступна для обслуживания.')
message_parts.append('') message_parts.append('')
message_parts.append(f'⏰ <i>{format_local_datetime(datetime.utcnow(), "%d.%m.%Y %H:%M:%S")}</i>') message_parts.append(f'⏰ <i>{format_local_datetime(datetime.now(UTC), "%d.%m.%Y %H:%M:%S")}</i>')
message = '\n'.join(message_parts) message = '\n'.join(message_parts)
@@ -1589,7 +1589,7 @@ class AdminNotificationService:
message_lines.extend( message_lines.extend(
[ [
'', '',
f'<i>{format_local_datetime(datetime.utcnow(), "%d.%m.%Y %H:%M")}</i>', f'<i>{format_local_datetime(datetime.now(UTC), "%d.%m.%Y %H:%M")}</i>',
] ]
) )
@@ -1662,7 +1662,7 @@ class AdminNotificationService:
message_lines.extend( message_lines.extend(
[ [
'', '',
f'⏰ <i>{format_local_datetime(datetime.utcnow(), "%d.%m.%Y %H:%M:%S")}</i>', f'⏰ <i>{format_local_datetime(datetime.now(UTC), "%d.%m.%Y %H:%M:%S")}</i>',
] ]
) )
+16 -14
View File
@@ -7,7 +7,7 @@ import shutil
import tarfile import tarfile
import tempfile import tempfile
from dataclasses import asdict, dataclass from dataclasses import asdict, dataclass
from datetime import date as dt_date, datetime, time as dt_time, timedelta from datetime import UTC, date as dt_date, datetime, time as dt_time, timedelta
from decimal import Decimal from decimal import Decimal
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -262,7 +262,7 @@ class BackupService:
return default_hours, default_minutes return default_hours, default_minutes
def _calculate_next_backup_datetime(self, reference: datetime | None = None) -> datetime: def _calculate_next_backup_datetime(self, reference: datetime | None = None) -> datetime:
reference = reference or datetime.now() reference = reference or datetime.now(UTC)
hours, minutes = self._parse_backup_time() hours, minutes = self._parse_backup_time()
next_run = reference.replace(hour=hours, minute=minutes, second=0, microsecond=0) next_run = reference.replace(hour=hours, minute=minutes, second=0, microsecond=0)
@@ -319,7 +319,7 @@ class BackupService:
overview = await self._collect_database_overview() overview = await self._collect_database_overview()
timestamp = datetime.utcnow().strftime('%Y%m%d_%H%M%S') timestamp = datetime.now(UTC).strftime('%Y%m%d_%H%M%S')
archive_suffix = '.tar.gz' if compress else '.tar' archive_suffix = '.tar.gz' if compress else '.tar'
filename = f'backup_{timestamp}{archive_suffix}' filename = f'backup_{timestamp}{archive_suffix}'
backup_path = self.backup_dir / filename backup_path = self.backup_dir / filename
@@ -337,7 +337,7 @@ class BackupService:
metadata = { metadata = {
'format_version': self.archive_format_version, 'format_version': self.archive_format_version,
'timestamp': datetime.utcnow().isoformat(), 'timestamp': datetime.now(UTC).isoformat(),
'database_type': 'postgresql' if settings.is_postgresql() else 'sqlite', 'database_type': 'postgresql' if settings.is_postgresql() else 'sqlite',
'backup_type': 'full', 'backup_type': 'full',
'tables_count': overview.get('tables_count', 0), 'tables_count': overview.get('tables_count', 0),
@@ -525,7 +525,7 @@ class BackupService:
dump_path = staging_dir / 'database.json' dump_path = staging_dir / 'database.json'
dump_structure = { dump_structure = {
'metadata': { 'metadata': {
'timestamp': datetime.utcnow().isoformat(), 'timestamp': datetime.now(UTC).isoformat(),
'version': 'orm-1.0', 'version': 'orm-1.0',
'database_type': 'postgresql', 'database_type': 'postgresql',
'tables_count': tables_count, 'tables_count': tables_count,
@@ -1171,10 +1171,10 @@ class BackupService:
if 'T' in value: if 'T' in value:
processed_data[key] = datetime.fromisoformat(value.replace('Z', '+00:00')) processed_data[key] = datetime.fromisoformat(value.replace('Z', '+00:00'))
else: else:
processed_data[key] = datetime.strptime(value, '%Y-%m-%d %H:%M:%S') processed_data[key] = datetime.strptime(value, '%Y-%m-%d %H:%M:%S').replace(tzinfo=UTC)
except (ValueError, TypeError) as e: except (ValueError, TypeError) as e:
logger.warning('Не удалось парсить дату для поля', value=value, key=key, error=e) logger.warning('Не удалось парсить дату для поля', value=value, key=key, error=e)
processed_data[key] = datetime.utcnow() processed_data[key] = datetime.now(UTC)
elif column_type_str == 'TIME' and isinstance(value, str): elif column_type_str == 'TIME' and isinstance(value, str):
try: try:
processed_data[key] = dt_time.fromisoformat(value) processed_data[key] = dt_time.fromisoformat(value)
@@ -1494,7 +1494,7 @@ class BackupService:
snapshots['app_config'] = { snapshots['app_config'] = {
'path': str(path_obj), 'path': str(path_obj),
'content': content, 'content': content,
'modified_at': datetime.fromtimestamp(path_obj.stat().st_mtime).isoformat(), 'modified_at': datetime.fromtimestamp(path_obj.stat().st_mtime, tz=UTC).isoformat(),
} }
logger.info('📁 Добавлен в бекап файл конфигурации', path_obj=path_obj) logger.info('📁 Добавлен в бекап файл конфигурации', path_obj=path_obj)
except Exception as e: except Exception as e:
@@ -1557,7 +1557,9 @@ class BackupService:
backup_info = { backup_info = {
'filename': backup_file.name, 'filename': backup_file.name,
'filepath': str(backup_file), 'filepath': str(backup_file),
'timestamp': metadata.get('timestamp', datetime.fromtimestamp(file_stats.st_mtime).isoformat()), 'timestamp': metadata.get(
'timestamp', datetime.fromtimestamp(file_stats.st_mtime, tz=UTC).isoformat()
),
'tables_count': metadata.get( 'tables_count': metadata.get(
'tables_count', metadata.get('database', {}).get('tables_count', 0) 'tables_count', metadata.get('database', {}).get('tables_count', 0)
), ),
@@ -1583,7 +1585,7 @@ class BackupService:
{ {
'filename': backup_file.name, 'filename': backup_file.name,
'filepath': str(backup_file), 'filepath': str(backup_file),
'timestamp': datetime.fromtimestamp(file_stats.st_mtime).isoformat(), 'timestamp': datetime.fromtimestamp(file_stats.st_mtime, tz=UTC).isoformat(),
'tables_count': '?', 'tables_count': '?',
'total_records': '?', 'total_records': '?',
'compressed': backup_file.suffix == '.gz', 'compressed': backup_file.suffix == '.gz',
@@ -1683,7 +1685,7 @@ class BackupService:
while True: while True:
try: try:
now = datetime.now() now = datetime.now(UTC)
delay = (next_run - now).total_seconds() delay = (next_run - now).total_seconds()
if delay > 0: if delay > 0:
@@ -1713,7 +1715,7 @@ class BackupService:
break break
except Exception as e: except Exception as e:
logger.error('Ошибка в цикле автобекапов', error=e) logger.error('Ошибка в цикле автобекапов', error=e)
next_run = datetime.now() + interval next_run = datetime.now(UTC) + interval
async def _send_backup_notification(self, event_type: str, message: str, file_path: str = None): async def _send_backup_notification(self, event_type: str, message: str, file_path: str = None):
try: try:
@@ -1728,7 +1730,7 @@ class BackupService:
if file_path: if file_path:
notification_text += f'\n📁 <code>{Path(file_path).name}</code>' notification_text += f'\n📁 <code>{Path(file_path).name}</code>'
notification_text += f'\n\n⏰ <i>{datetime.now().strftime("%d.%m.%Y %H:%M:%S")}</i>' notification_text += f'\n\n⏰ <i>{datetime.now(UTC).strftime("%d.%m.%Y %H:%M:%S")}</i>'
try: try:
from app.services.admin_notification_service import AdminNotificationService from app.services.admin_notification_service import AdminNotificationService
@@ -1762,7 +1764,7 @@ class BackupService:
caption = '📦 <b>Резервная копия</b>\n\n' caption = '📦 <b>Резервная копия</b>\n\n'
if temp_zip_path: if temp_zip_path:
caption += '🔐 <b>Архив защищён паролем</b>\n\n' caption += '🔐 <b>Архив защищён паролем</b>\n\n'
caption += f'⏰ <i>{datetime.now().strftime("%d.%m.%Y %H:%M:%S")}</i>' caption += f'⏰ <i>{datetime.now(UTC).strftime("%d.%m.%Y %H:%M:%S")}</i>'
send_kwargs = { send_kwargs = {
'chat_id': chat_id, 'chat_id': chat_id,
+4 -4
View File
@@ -5,7 +5,7 @@
import asyncio import asyncio
import time import time
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
import aiohttp import aiohttp
import structlog import structlog
@@ -122,7 +122,7 @@ class BlacklistService:
) )
self.blacklist_data = blacklist_data self.blacklist_data = blacklist_data
self.last_update = datetime.utcnow() self.last_update = datetime.now(UTC)
self._check_cache.clear() self._check_cache.clear()
logger.info('Черный список успешно обновлен. Найдено записей', blacklist_data_count=len(blacklist_data)) logger.info('Черный список успешно обновлен. Найдено записей', blacklist_data_count=len(blacklist_data))
return True return True
@@ -164,7 +164,7 @@ class BlacklistService:
# Если черный список пуст или устарел, обновляем его # Если черный список пуст или устарел, обновляем его
interval_hours = self.get_blacklist_update_interval_hours() interval_hours = self.get_blacklist_update_interval_hours()
required_interval = timedelta(hours=interval_hours) required_interval = timedelta(hours=interval_hours)
if not self.blacklist_data or (self.last_update and datetime.utcnow() - self.last_update > required_interval): if not self.blacklist_data or (self.last_update and datetime.now(UTC) - self.last_update > required_interval):
await self.update_blacklist() await self.update_blacklist()
# Проверяем по Telegram ID # Проверяем по Telegram ID
@@ -197,7 +197,7 @@ class BlacklistService:
""" """
interval_hours = self.get_blacklist_update_interval_hours() interval_hours = self.get_blacklist_update_interval_hours()
required_interval = timedelta(hours=interval_hours) required_interval = timedelta(hours=interval_hours)
if not self.blacklist_data or (self.last_update and datetime.utcnow() - self.last_update > required_interval): if not self.blacklist_data or (self.last_update and datetime.now(UTC) - self.last_update > required_interval):
await self.update_blacklist() await self.update_blacklist()
return self.blacklist_data.copy() return self.blacklist_data.copy()
+3 -3
View File
@@ -2,7 +2,7 @@ from __future__ import annotations
import asyncio import asyncio
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import UTC, datetime
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import structlog import structlog
@@ -467,7 +467,7 @@ class BroadcastService:
broadcast.status = status broadcast.status = status
if update_completed_at: if update_completed_at:
broadcast.completed_at = datetime.utcnow() broadcast.completed_at = datetime.now(UTC)
await session.commit() await session.commit()
return return
@@ -865,7 +865,7 @@ class EmailBroadcastService:
broadcast.status = status broadcast.status = status
if update_completed_at: if update_completed_at:
broadcast.completed_at = datetime.utcnow() broadcast.completed_at = datetime.now(UTC)
await session.commit() await session.commit()
return return
+2 -2
View File
@@ -221,8 +221,8 @@ class ContestRotationService:
continue continue
# Convert to UTC for storage # Convert to UTC for storage
starts_at_utc = starts_at_local.astimezone(UTC).replace(tzinfo=None) starts_at_utc = starts_at_local.astimezone(UTC)
ends_at_utc = ends_at_local.astimezone(UTC).replace(tzinfo=None) ends_at_utc = ends_at_local.astimezone(UTC)
# Анонс перед созданием раунда # Анонс перед созданием раунда
await self._announce_round_start(tpl, starts_at_local, ends_at_local) await self._announce_round_start(tpl, starts_at_local, ends_at_local)
+4 -4
View File
@@ -5,7 +5,7 @@
""" """
import asyncio import asyncio
from datetime import datetime from datetime import UTC, datetime
import structlog import structlog
from aiogram import Bot from aiogram import Bot
@@ -290,7 +290,7 @@ class DailySubscriptionService:
async with AsyncSessionLocal() as db: async with AsyncSessionLocal() as db:
try: try:
# Находим все истекшие докупки # Находим все истекшие докупки
now = datetime.utcnow() now = datetime.now(UTC)
query = select(TrafficPurchase).where(TrafficPurchase.expires_at <= now) query = select(TrafficPurchase).where(TrafficPurchase.expires_at <= now)
result = await db.execute(query) result = await db.execute(query)
expired_purchases = result.scalars().all() expired_purchases = result.scalars().all()
@@ -400,7 +400,7 @@ class DailySubscriptionService:
subscription.purchased_traffic_gb = max(0, new_purchased) subscription.purchased_traffic_gb = max(0, new_purchased)
# Проверяем, остались ли активные докупки # Проверяем, остались ли активные докупки
now = datetime.utcnow() now = datetime.now(UTC)
remaining_query = ( remaining_query = (
select(TrafficPurchase) select(TrafficPurchase)
.where(TrafficPurchase.subscription_id == subscription_id) .where(TrafficPurchase.subscription_id == subscription_id)
@@ -417,7 +417,7 @@ class DailySubscriptionService:
next_expiry = min(p.expires_at for p in remaining_purchases) next_expiry = min(p.expires_at for p in remaining_purchases)
subscription.traffic_reset_at = next_expiry subscription.traffic_reset_at = next_expiry
subscription.updated_at = datetime.utcnow() subscription.updated_at = datetime.now(UTC)
await db.commit() await db.commit()
+2 -2
View File
@@ -3,7 +3,7 @@ from __future__ import annotations
import asyncio import asyncio
import json import json
from collections.abc import Callable from collections.abc import Callable
from datetime import datetime from datetime import UTC, datetime
from typing import Any from typing import Any
import structlog import structlog
@@ -60,7 +60,7 @@ class EventEmitter:
event_data = { event_data = {
'type': event_type, 'type': event_type,
'payload': payload, 'payload': payload,
'timestamp': str(datetime.utcnow()), 'timestamp': str(datetime.now(UTC)),
} }
# Вызываем локальные слушатели # Вызываем локальные слушатели
+10 -10
View File
@@ -1,6 +1,6 @@
import asyncio import asyncio
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import UTC, datetime
from typing import Any from typing import Any
import structlog import structlog
@@ -76,7 +76,7 @@ class MaintenanceService:
emoji_map = {'error': '🚨', 'warning': '⚠️', 'success': '', 'info': ''} emoji_map = {'error': '🚨', 'warning': '⚠️', 'success': '', 'info': ''}
emoji = emoji_map.get(alert_type, '') emoji = emoji_map.get(alert_type, '')
timestamp = format_local_datetime(datetime.utcnow(), '%d.%m.%Y %H:%M:%S %Z') timestamp = format_local_datetime(datetime.now(UTC), '%d.%m.%Y %H:%M:%S %Z')
formatted_message = f'{emoji} <b>ТЕХНИЧЕСКИЕ РАБОТЫ</b>\n\n{message}\n\n⏰ <i>{timestamp}</i>' formatted_message = f'{emoji} <b>ТЕХНИЧЕСКИЕ РАБОТЫ</b>\n\n{message}\n\n⏰ <i>{timestamp}</i>'
return await notification_service._send_message(formatted_message) return await notification_service._send_message(formatted_message)
@@ -135,7 +135,7 @@ class MaintenanceService:
return True return True
self._status.is_active = True self._status.is_active = True
self._status.enabled_at = datetime.utcnow() self._status.enabled_at = datetime.now(UTC)
self._status.reason = reason or ('Автоматическое включение' if auto else 'Включено администратором') self._status.reason = reason or ('Автоматическое включение' if auto else 'Включено администратором')
self._status.auto_enabled = auto self._status.auto_enabled = auto
@@ -168,7 +168,7 @@ class MaintenanceService:
was_auto = self._status.auto_enabled was_auto = self._status.auto_enabled
duration = None duration = None
if self._status.enabled_at: if self._status.enabled_at:
duration = datetime.utcnow() - self._status.enabled_at duration = datetime.now(UTC) - self._status.enabled_at
self._status.is_active = False self._status.is_active = False
self._status.enabled_at = None self._status.enabled_at = None
@@ -187,7 +187,7 @@ class MaintenanceService:
else: else:
duration_str = f'\n⏱️ <b>Длительность:</b> {minutes}мин' duration_str = f'\n⏱️ <b>Длительность:</b> {minutes}мин'
notification_time = format_local_datetime(datetime.utcnow(), '%d.%m.%Y %H:%M:%S %Z') notification_time = format_local_datetime(datetime.now(UTC), '%d.%m.%Y %H:%M:%S %Z')
notification_msg = f"""Режим технических работ ВЫКЛЮЧЕН notification_msg = f"""Режим технических работ ВЫКЛЮЧЕН
🤖 <b>Автоматически:</b> {'Да' if was_auto else 'Нет'} 🤖 <b>Автоматически:</b> {'Да' if was_auto else 'Нет'}
@@ -252,7 +252,7 @@ class MaintenanceService:
return self._status.api_status return self._status.api_status
self._is_checking = True self._is_checking = True
self._status.last_check = datetime.utcnow() self._status.last_check = datetime.now(UTC)
auth_params = settings.get_remnawave_auth_params() auth_params = settings.get_remnawave_auth_params()
base_url = (auth_params.get('base_url') or '').strip() base_url = (auth_params.get('base_url') or '').strip()
@@ -356,7 +356,7 @@ API снова отвечает на запросы.""",
logger.error('Ошибка проверки API', error=e) logger.error('Ошибка проверки API', error=e)
if self._status.api_status: if self._status.api_status:
error_time = format_local_datetime(datetime.utcnow(), '%H:%M:%S %Z') error_time = format_local_datetime(datetime.now(UTC), '%H:%M:%S %Z')
await self._notify_admins( await self._notify_admins(
f"""Ошибка при проверке API Remnawave f"""Ошибка при проверке API Remnawave
@@ -442,11 +442,11 @@ API снова отвечает на запросы.""",
} }
async def force_api_check(self) -> dict[str, Any]: async def force_api_check(self) -> dict[str, Any]:
start_time = datetime.utcnow() start_time = datetime.now(UTC)
try: try:
api_status = await self.check_api_status() api_status = await self.check_api_status()
end_time = datetime.utcnow() end_time = datetime.now(UTC)
response_time = (end_time - start_time).total_seconds() response_time = (end_time - start_time).total_seconds()
return { return {
@@ -458,7 +458,7 @@ API снова отвечает на запросы.""",
} }
except Exception as e: except Exception as e:
end_time = datetime.utcnow() end_time = datetime.now(UTC)
response_time = (end_time - start_time).total_seconds() response_time = (end_time - start_time).total_seconds()
return { return {
+9 -17
View File
@@ -12,14 +12,6 @@ from app.config import settings
from app.database.models import ButtonClickLog from app.database.models import ButtonClickLog
def _utcnow() -> datetime:
"""Возвращает текущее UTC время как naive datetime (без timezone).
Замена deprecated datetime.utcnow().
"""
return datetime.now(UTC).replace(tzinfo=None)
class MenuLayoutStatsService: class MenuLayoutStatsService:
"""Сервис для сбора и анализа статистики кликов по кнопкам.""" """Сервис для сбора и анализа статистики кликов по кнопкам."""
@@ -104,7 +96,7 @@ class MenuLayoutStatsService:
days: int = 30, days: int = 30,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Получить статистику кликов по конкретной кнопке.""" """Получить статистику кликов по конкретной кнопке."""
now = _utcnow() now = datetime.now(UTC)
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
week_ago = now - timedelta(days=7) week_ago = now - timedelta(days=7)
month_ago = now - timedelta(days=days) month_ago = now - timedelta(days=days)
@@ -172,7 +164,7 @@ class MenuLayoutStatsService:
days: int = 30, days: int = 30,
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
"""Получить статистику кликов по дням.""" """Получить статистику кликов по дням."""
start_date = _utcnow() - timedelta(days=days) start_date = datetime.now(UTC) - timedelta(days=days)
# Группировка по дате # Группировка по дате
result = await db.execute( result = await db.execute(
@@ -191,7 +183,7 @@ class MenuLayoutStatsService:
days: int = 30, days: int = 30,
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
"""Получить статистику по всем кнопкам.""" """Получить статистику по всем кнопкам."""
now = _utcnow() now = datetime.now(UTC)
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
week_ago = now - timedelta(days=7) week_ago = now - timedelta(days=7)
month_ago = now - timedelta(days=days) month_ago = now - timedelta(days=days)
@@ -237,7 +229,7 @@ class MenuLayoutStatsService:
days: int = 30, days: int = 30,
) -> int: ) -> int:
"""Получить общее количество кликов за период.""" """Получить общее количество кликов за период."""
start_date = _utcnow() - timedelta(days=days) start_date = datetime.now(UTC) - timedelta(days=days)
result = await db.execute(select(func.count(ButtonClickLog.id)).where(ButtonClickLog.clicked_at >= start_date)) result = await db.execute(select(func.count(ButtonClickLog.id)).where(ButtonClickLog.clicked_at >= start_date))
return result.scalar() or 0 return result.scalar() or 0
@@ -249,7 +241,7 @@ class MenuLayoutStatsService:
days: int = 30, days: int = 30,
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
"""Получить статистику кликов по типам кнопок.""" """Получить статистику кликов по типам кнопок."""
start_date = _utcnow() - timedelta(days=days) start_date = datetime.now(UTC) - timedelta(days=days)
result = await db.execute( result = await db.execute(
select( select(
@@ -279,7 +271,7 @@ class MenuLayoutStatsService:
days: int = 30, days: int = 30,
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
"""Получить статистику кликов по часам дня.""" """Получить статистику кликов по часам дня."""
start_date = _utcnow() - timedelta(days=days) start_date = datetime.now(UTC) - timedelta(days=days)
# Используем helper-метод для совместимости с SQLite и PostgreSQL # Используем helper-метод для совместимости с SQLite и PostgreSQL
hour_expr = cls._get_hour_expr(ButtonClickLog.clicked_at).label('hour') hour_expr = cls._get_hour_expr(ButtonClickLog.clicked_at).label('hour')
@@ -311,7 +303,7 @@ class MenuLayoutStatsService:
Возвращает 0=понедельник, 6=воскресенье. Возвращает 0=понедельник, 6=воскресенье.
Поддерживает как PostgreSQL, так и SQLite. Поддерживает как PostgreSQL, так и SQLite.
""" """
start_date = _utcnow() - timedelta(days=days) start_date = datetime.now(UTC) - timedelta(days=days)
# Используем helper-метод для совместимости с SQLite и PostgreSQL # Используем helper-метод для совместимости с SQLite и PostgreSQL
weekday_expr = cls._get_weekday_expr(ButtonClickLog.clicked_at).label('weekday') weekday_expr = cls._get_weekday_expr(ButtonClickLog.clicked_at).label('weekday')
@@ -345,7 +337,7 @@ class MenuLayoutStatsService:
days: int = 30, days: int = 30,
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
"""Получить топ пользователей по количеству кликов.""" """Получить топ пользователей по количеству кликов."""
start_date = _utcnow() - timedelta(days=days) start_date = datetime.now(UTC) - timedelta(days=days)
query = select( query = select(
ButtonClickLog.user_id, ButtonClickLog.user_id,
@@ -378,7 +370,7 @@ class MenuLayoutStatsService:
previous_days: int = 7, previous_days: int = 7,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Сравнить статистику текущего и предыдущего периода.""" """Сравнить статистику текущего и предыдущего периода."""
now = _utcnow() now = datetime.now(UTC)
current_start = now - timedelta(days=current_days) current_start = now - timedelta(days=current_days)
previous_start = current_start - timedelta(days=previous_days) previous_start = current_start - timedelta(days=previous_days)
previous_end = current_start previous_end = current_start

Some files were not shown because too many files have changed in this diff Show More