From eb18994b7d34d777ca39d3278d509e41359e2a85 Mon Sep 17 00:00:00 2001 From: Fringg Date: Tue, 17 Feb 2026 04:45:40 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20complete=20datetime.utcnow()=20=E2=86=92?= =?UTF-8?q?=20datetime.now(UTC)=20migration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- app/cabinet/auth/email_verification.py | 10 +- app/cabinet/auth/jwt_handler.py | 12 +- app/cabinet/auth/telegram_auth.py | 10 +- app/cabinet/routes/admin_broadcasts.py | 4 +- app/cabinet/routes/admin_pinned_messages.py | 10 +- app/cabinet/routes/admin_promocodes.py | 4 +- app/cabinet/routes/admin_remnawave.py | 4 +- app/cabinet/routes/admin_stats.py | 10 +- app/cabinet/routes/admin_tickets.py | 12 +- app/cabinet/routes/admin_updates.py | 6 +- app/cabinet/routes/admin_users.py | 95 ++--- app/cabinet/routes/auth.py | 22 +- app/cabinet/routes/contests.py | 4 +- app/cabinet/routes/notifications.py | 4 +- app/cabinet/routes/oauth.py | 2 +- app/cabinet/routes/polls.py | 6 +- app/cabinet/routes/promo.py | 10 +- app/cabinet/routes/subscription.py | 60 +-- app/cabinet/routes/tickets.py | 12 +- .../services/email_template_overrides.py | 4 +- app/database/crud/campaign.py | 4 +- app/database/crud/cloudpayments.py | 8 +- app/database/crud/contest.py | 6 +- app/database/crud/cryptobot.py | 10 +- app/database/crud/discount_offer.py | 10 +- app/database/crud/faq.py | 8 +- app/database/crud/freekassa.py | 8 +- app/database/crud/heleket.py | 6 +- app/database/crud/kassa_ai.py | 8 +- app/database/crud/mulenpay.py | 8 +- app/database/crud/platega.py | 6 +- app/database/crud/privacy_policy.py | 6 +- app/database/crud/promo_offer_template.py | 8 +- app/database/crud/promocode.py | 12 +- app/database/crud/public_offer.py | 6 +- app/database/crud/referral.py | 14 +- app/database/crud/referral_contest.py | 6 +- app/database/crud/rules.py | 8 +- app/database/crud/server_squad.py | 6 +- app/database/crud/subscription.py | 78 ++-- app/database/crud/subscription_conversion.py | 6 +- app/database/crud/subscription_event.py | 4 +- app/database/crud/ticket.py | 18 +- app/database/crud/ticket_notification.py | 12 +- app/database/crud/transaction.py | 18 +- app/database/crud/user.py | 36 +- app/database/crud/user_message.py | 6 +- app/database/crud/user_promo_group.py | 4 +- app/database/crud/web_api_token.py | 6 +- app/database/crud/webhook.py | 8 +- app/database/crud/welcome_text.py | 6 +- app/database/crud/wheel.py | 12 +- app/database/crud/yookassa.py | 6 +- app/database/models.py | 369 +++++++++--------- app/database/universal_migration.py | 68 +++- app/external/wata_webhook.py | 8 +- app/handlers/admin/bot_configuration.py | 6 +- app/handlers/admin/contests.py | 6 +- app/handlers/admin/daily_contests.py | 20 +- app/handlers/admin/messages.py | 40 +- app/handlers/admin/monitoring.py | 23 +- app/handlers/admin/payments.py | 4 +- app/handlers/admin/promo_offers.py | 8 +- app/handlers/admin/promocodes.py | 8 +- app/handlers/admin/referrals.py | 11 +- app/handlers/admin/remnawave.py | 8 +- app/handlers/admin/statistics.py | 20 +- app/handlers/admin/system_logs.py | 6 +- app/handlers/admin/tickets.py | 6 +- app/handlers/admin/users.py | 32 +- app/handlers/balance/heleket.py | 4 +- app/handlers/balance/pal24.py | 4 +- app/handlers/balance/wata.py | 4 +- app/handlers/balance/yookassa.py | 6 +- app/handlers/contests.py | 4 +- app/handlers/menu.py | 8 +- app/handlers/polls.py | 6 +- app/handlers/server_status.py | 4 +- app/handlers/simple_subscription.py | 4 +- app/handlers/start.py | 40 +- app/handlers/subscription/countries.py | 6 +- app/handlers/subscription/devices.py | 8 +- app/handlers/subscription/pricing.py | 4 +- app/handlers/subscription/promo.py | 4 +- app/handlers/subscription/purchase.py | 22 +- app/handlers/subscription/tariff_purchase.py | 36 +- app/handlers/subscription/traffic.py | 6 +- app/keyboards/inline.py | 8 +- app/middlewares/auth.py | 6 +- app/middlewares/channel_checker.py | 4 +- app/middlewares/subscription_checker.py | 4 +- app/services/admin_notification_service.py | 44 +-- app/services/backup_service.py | 30 +- app/services/blacklist_service.py | 8 +- app/services/broadcast_service.py | 6 +- app/services/contest_rotation_service.py | 4 +- app/services/daily_subscription_service.py | 8 +- app/services/event_emitter.py | 4 +- app/services/maintenance_service.py | 20 +- app/services/menu_layout/stats_service.py | 26 +- app/services/monitoring_service.py | 44 +-- app/services/nalogo_queue_service.py | 8 +- app/services/nalogo_service.py | 15 +- app/services/pal24_service.py | 4 +- app/services/partner_stats_service.py | 16 +- app/services/payment/cloudpayments.py | 4 +- app/services/payment/common.py | 4 +- app/services/payment/cryptobot.py | 12 +- app/services/payment/freekassa.py | 6 +- app/services/payment/heleket.py | 10 +- app/services/payment/kassa_ai.py | 6 +- app/services/payment/pal24.py | 8 +- app/services/payment/platega.py | 9 +- app/services/payment/stars.py | 6 +- app/services/payment/wata.py | 4 +- app/services/payment/yookassa.py | 20 +- app/services/payment_verification_service.py | 8 +- app/services/pinned_message_service.py | 6 +- app/services/platega_service.py | 4 +- app/services/promo_group_assignment.py | 6 +- app/services/promo_offer_service.py | 6 +- app/services/promocode_service.py | 16 +- app/services/referral_contest_service.py | 34 +- app/services/referral_diagnostics_service.py | 42 +- app/services/referral_withdrawal_service.py | 18 +- app/services/remnawave_service.py | 64 ++- app/services/remnawave_sync_service.py | 18 +- app/services/remnawave_webhook_service.py | 12 +- app/services/reporting_service.py | 6 +- app/services/startup_notification_service.py | 8 +- .../subscription_auto_purchase_service.py | 14 +- app/services/subscription_purchase_service.py | 4 +- app/services/subscription_service.py | 4 +- app/services/traffic_monitoring_service.py | 36 +- app/services/tribute_service.py | 8 +- app/services/user_service.py | 18 +- app/services/version_service.py | 6 +- app/services/wata_service.py | 6 +- app/services/web_api_token_service.py | 10 +- app/services/wheel_service.py | 10 +- app/utils/currency_converter.py | 4 +- app/utils/formatters.py | 20 +- app/utils/pricing_utils.py | 4 +- app/utils/promo_offer.py | 8 +- app/utils/subscription_utils.py | 4 +- app/utils/timezone.py | 10 +- app/utils/user_utils.py | 14 +- app/webapi/background/backup_tasks.py | 10 +- app/webapi/routes/broadcasts.py | 4 +- app/webapi/routes/contests.py | 6 +- app/webapi/routes/menu_layout.py | 8 +- app/webapi/routes/miniapp.py | 48 +-- app/webapi/routes/pinned_messages.py | 10 +- app/webapi/routes/promocodes.py | 4 +- app/webapi/routes/remnawave.py | 4 +- app/webapi/routes/stats.py | 4 +- app/webapi/routes/tickets.py | 8 +- pyproject.toml | 1 - tests/conftest.py | 2 +- tests/fixtures/promocode_fixtures.py | 22 +- tests/services/test_pal24_service_adapter.py | 4 +- .../test_payment_service_cryptobot.py | 4 +- .../services/test_payment_service_heleket.py | 4 +- .../services/test_payment_service_mulenpay.py | 4 +- tests/services/test_payment_service_pal24.py | 4 +- .../services/test_payment_service_platega.py | 4 +- tests/services/test_payment_service_stars.py | 4 +- tests/services/test_payment_service_wata.py | 12 +- .../services/test_payment_service_yookassa.py | 4 +- tests/services/test_referral_diagnostics.py | 14 +- ...test_subscription_auto_purchase_service.py | 12 +- .../services/test_traffic_monitoring_redis.py | 24 +- .../services/test_yookassa_service_adapter.py | 6 +- tests/test_menu_subscription_status.py | 4 +- tests/test_miniapp_payments.py | 26 +- tests/utils/test_formatters_basic.py | 8 +- tests/utils/test_remnawave_auto_sync.py | 14 +- 177 files changed, 1280 insertions(+), 1261 deletions(-) diff --git a/app/cabinet/auth/email_verification.py b/app/cabinet/auth/email_verification.py index 327549b1..45e49cc9 100644 --- a/app/cabinet/auth/email_verification.py +++ b/app/cabinet/auth/email_verification.py @@ -1,7 +1,7 @@ """Email verification token generation and validation.""" import secrets -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from app.config import settings @@ -24,7 +24,7 @@ def get_email_change_expires_at() -> datetime: Datetime when the email change code expires """ 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: @@ -55,7 +55,7 @@ def get_verification_expires_at() -> datetime: Datetime when the verification token expires """ 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: @@ -66,7 +66,7 @@ def get_password_reset_expires_at() -> datetime: Datetime when the password reset token expires """ 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: @@ -81,4 +81,4 @@ def is_token_expired(expires_at: datetime | None) -> bool: """ if expires_at is None: return True - return datetime.utcnow() > expires_at + return datetime.now(UTC) > expires_at diff --git a/app/cabinet/auth/jwt_handler.py b/app/cabinet/auth/jwt_handler.py index ef430608..7100a8f7 100644 --- a/app/cabinet/auth/jwt_handler.py +++ b/app/cabinet/auth/jwt_handler.py @@ -1,6 +1,6 @@ """JWT token handling for cabinet authentication.""" -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from typing import Any import jwt @@ -23,13 +23,13 @@ def create_access_token(user_id: int, telegram_id: int | None = None) -> str: Encoded JWT access token """ 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 = { 'sub': str(user_id), 'type': 'access', 'exp': expires, - 'iat': datetime.utcnow(), + 'iat': datetime.now(UTC), } # Добавляем telegram_id только если он есть @@ -51,13 +51,13 @@ def create_refresh_token(user_id: int) -> str: Encoded JWT refresh token """ 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 = { 'sub': str(user_id), 'type': 'refresh', 'exp': expires, - 'iat': datetime.utcnow(), + 'iat': datetime.now(UTC), } 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: """Get the expiration datetime for a new refresh token.""" expire_days = settings.get_cabinet_refresh_token_expire_days() - return datetime.utcnow() + timedelta(days=expire_days) + return datetime.now(UTC) + timedelta(days=expire_days) diff --git a/app/cabinet/auth/telegram_auth.py b/app/cabinet/auth/telegram_auth.py index c9b841b5..7340ba09 100644 --- a/app/cabinet/auth/telegram_auth.py +++ b/app/cabinet/auth/telegram_auth.py @@ -3,7 +3,7 @@ import hashlib import hmac import json -from datetime import datetime +from datetime import UTC, datetime from typing import Any 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: try: # Use UTC timestamp to avoid timezone issues - auth_time = datetime.utcfromtimestamp(int(auth_date)) - age = (datetime.utcnow() - auth_time).total_seconds() + auth_time = datetime.fromtimestamp(int(auth_date), tz=UTC) + age = (datetime.now(UTC) - auth_time).total_seconds() if age > max_age_seconds: return False except (ValueError, TypeError, OSError): @@ -81,8 +81,8 @@ def validate_telegram_init_data(init_data: str, max_age_seconds: int = 86400) -> if auth_date: try: # Use UTC timestamp to avoid timezone issues - auth_time = datetime.utcfromtimestamp(int(auth_date)) - age = (datetime.utcnow() - auth_time).total_seconds() + auth_time = datetime.fromtimestamp(int(auth_date), tz=UTC) + age = (datetime.now(UTC) - auth_time).total_seconds() if age > max_age_seconds: return None except (ValueError, TypeError, OSError): diff --git a/app/cabinet/routes/admin_broadcasts.py b/app/cabinet/routes/admin_broadcasts.py index 099b1228..ed7f9876 100644 --- a/app/cabinet/routes/admin_broadcasts.py +++ b/app/cabinet/routes/admin_broadcasts.py @@ -1,6 +1,6 @@ """Admin routes for broadcasts in cabinet.""" -from datetime import datetime +from datetime import UTC, datetime import structlog from fastapi import APIRouter, Depends, HTTPException, Query, status @@ -724,7 +724,7 @@ async def stop_broadcast( broadcast.status = 'cancelling' else: broadcast.status = 'cancelled' - broadcast.completed_at = datetime.utcnow() + broadcast.completed_at = datetime.now(UTC) await db.commit() await db.refresh(broadcast) diff --git a/app/cabinet/routes/admin_pinned_messages.py b/app/cabinet/routes/admin_pinned_messages.py index 16f70839..120fc848 100644 --- a/app/cabinet/routes/admin_pinned_messages.py +++ b/app/cabinet/routes/admin_pinned_messages.py @@ -1,7 +1,7 @@ """Admin routes for pinned messages in cabinet.""" import time -from datetime import datetime +from datetime import UTC, datetime import structlog from aiogram import Bot @@ -227,7 +227,7 @@ async def update_pinned_message( if payload.send_on_every_start is not None: 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.refresh(msg) @@ -255,7 +255,7 @@ async def update_pinned_message_settings( if payload.send_on_every_start is not None: 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.refresh(msg) @@ -332,11 +332,11 @@ async def activate_pinned_message( await db.execute( update(PinnedMessage) .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.updated_at = datetime.utcnow() + msg.updated_at = datetime.now(UTC) await db.commit() await db.refresh(msg) diff --git a/app/cabinet/routes/admin_promocodes.py b/app/cabinet/routes/admin_promocodes.py index 9d8ffcd1..190912bf 100644 --- a/app/cabinet/routes/admin_promocodes.py +++ b/app/cabinet/routes/admin_promocodes.py @@ -162,9 +162,9 @@ def _normalize_datetime(value: datetime | None) -> datetime | None: if value is None: return 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: - return value.replace(tzinfo=None) + return value return value diff --git a/app/cabinet/routes/admin_remnawave.py b/app/cabinet/routes/admin_remnawave.py index 8d7be615..4425ad19 100644 --- a/app/cabinet/routes/admin_remnawave.py +++ b/app/cabinet/routes/admin_remnawave.py @@ -1,6 +1,6 @@ """Admin routes for RemnaWave management in cabinet.""" -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from typing import Any import structlog @@ -338,7 +338,7 @@ async def get_node_usage( service = _get_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)) if start_dt >= end_dt: diff --git a/app/cabinet/routes/admin_stats.py b/app/cabinet/routes/admin_stats.py index deda96c9..4f7136cf 100644 --- a/app/cabinet/routes/admin_stats.py +++ b/app/cabinet/routes/admin_stats.py @@ -2,7 +2,7 @@ import sys import time -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta import structlog from fastapi import APIRouter, Depends, HTTPException, status @@ -258,7 +258,7 @@ async def get_dashboard_stats( sub_stats = await get_subscriptions_statistics(db) # Get financial statistics - now = datetime.utcnow() + now = datetime.now(UTC) month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) 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('📊 Нет тарифов в системе, пропускаем статистику') return None - now = datetime.utcnow() + now = datetime.now(UTC) today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) week_ago = now - timedelta(days=7) month_ago = now - timedelta(days=30) @@ -601,7 +601,7 @@ async def get_top_referrers( ): """Get top referrers with earnings breakdown by period.""" try: - now = datetime.utcnow() + now = datetime.now(UTC) today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) week_ago = now - timedelta(days=7) month_ago = now - timedelta(days=30) @@ -874,7 +874,7 @@ async def get_recent_payments( ): """Get recent payments with user info.""" try: - now = datetime.utcnow() + now = datetime.now(UTC) today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) week_ago = now - timedelta(days=7) diff --git a/app/cabinet/routes/admin_tickets.py b/app/cabinet/routes/admin_tickets.py index c93a370c..a0f7cf4f 100644 --- a/app/cabinet/routes/admin_tickets.py +++ b/app/cabinet/routes/admin_tickets.py @@ -1,7 +1,7 @@ """Admin tickets routes for cabinet.""" import math -from datetime import datetime +from datetime import UTC, datetime import structlog from fastapi import APIRouter, Depends, HTTPException, Query, status @@ -447,13 +447,13 @@ async def reply_to_ticket( user_id=ticket.user_id, message_text=request.message, is_from_admin=True, - created_at=datetime.utcnow(), + created_at=datetime.now(UTC), ) db.add(message) # Update ticket status to answered ticket.status = 'answered' - ticket.updated_at = datetime.utcnow() + ticket.updated_at = datetime.now(UTC) await db.commit() await db.refresh(message) @@ -522,9 +522,9 @@ async def update_ticket_status( ) ticket.status = request.status - ticket.updated_at = datetime.utcnow() + ticket.updated_at = datetime.now(UTC) if request.status == 'closed': - ticket.closed_at = datetime.utcnow() + ticket.closed_at = datetime.now(UTC) else: ticket.closed_at = None @@ -581,7 +581,7 @@ async def update_ticket_priority( ) ticket.priority = request.priority - ticket.updated_at = datetime.utcnow() + ticket.updated_at = datetime.now(UTC) await db.commit() await db.refresh(ticket) diff --git a/app/cabinet/routes/admin_updates.py b/app/cabinet/routes/admin_updates.py index 1e5ce846..5b3cfd3c 100644 --- a/app/cabinet/routes/admin_updates.py +++ b/app/cabinet/routes/admin_updates.py @@ -1,6 +1,6 @@ """Admin routes for version and release information.""" -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta import aiohttp import structlog @@ -53,7 +53,7 @@ async def _fetch_cabinet_releases(force: bool = False) -> list[dict]: global _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'] 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_last_check = datetime.now() + _cabinet_last_check = datetime.now(UTC) logger.info('Fetched cabinet releases from GitHub', releases_count=len(releases)) return releases logger.warning('GitHub API returned status for cabinet releases', response_status=response.status) diff --git a/app/cabinet/routes/admin_users.py b/app/cabinet/routes/admin_users.py index 6fc13f6c..fcf079e3 100644 --- a/app/cabinet/routes/admin_users.py +++ b/app/cabinet/routes/admin_users.py @@ -1,6 +1,6 @@ """Admin routes for managing users in cabinet.""" -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta import structlog from fastapi import APIRouter, Depends, HTTPException, Query, status @@ -35,7 +35,7 @@ from app.database.models import ( User, 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 ..schemas.users import ( @@ -144,9 +144,9 @@ def _build_subscription_info(subscription: Subscription, tariff_name: str | None is_active = False if subscription.end_date: - delta = subscription.end_date - datetime.utcnow() + delta = subscription.end_date - datetime.now(UTC) 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( id=subscription.id, @@ -174,7 +174,7 @@ async def _build_subscription_info_async(db: AsyncSession, subscription: Subscri tariff_name = tariff.name # Fetch traffic purchases - now = datetime.utcnow() + now = datetime.now(UTC) tp_query = ( select(TrafficPurchase) .where(TrafficPurchase.subscription_id == subscription.id) @@ -225,13 +225,13 @@ async def _sync_subscription_to_panel(db: AsyncSession, user: User, subscription is_active = ( subscription.status in (SubscriptionStatus.ACTIVE.value, SubscriptionStatus.TRIAL.value) 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 expire_at = subscription.end_date - if expire_at and expire_at <= datetime.utcnow(): - expire_at = datetime.utcnow() + timedelta(minutes=1) + if expire_at and expire_at <= datetime.now(UTC): + expire_at = datetime.now(UTC) + timedelta(minutes=1) username = settings.format_remnawave_username( full_name=user.full_name, @@ -310,7 +310,7 @@ async def _sync_subscription_to_panel(db: AsyncSession, user: User, subscription # Create new user create_kwargs = { '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, 'traffic_limit_bytes': traffic_limit_bytes, '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 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() return changes @@ -421,7 +421,7 @@ async def get_users_stats( func.cast( and_( Subscription.status == SubscriptionStatus.ACTIVE.value, - Subscription.end_date > datetime.utcnow(), + Subscription.end_date > datetime.now(UTC), ), Integer, ) @@ -431,7 +431,7 @@ async def get_users_stats( func.cast( or_( Subscription.status == SubscriptionStatus.EXPIRED.value, - Subscription.end_date <= datetime.utcnow(), + Subscription.end_date <= datetime.now(UTC), ), Integer, ) @@ -456,7 +456,7 @@ async def get_users_stats( avg_balance = int(balance_row.avg or 0) if balance_row else 0 # Get activity stats - now = datetime.utcnow() + now = datetime.now(UTC) today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) week_ago = now - timedelta(days=7) month_ago = now - timedelta(days=30) @@ -756,7 +756,7 @@ async def get_user_node_usage( if not service.is_configured: return UserNodeUsageResponse(items=[]) - end_date = datetime.utcnow() + end_date = datetime.now(UTC) start_date = end_date - timedelta(days=30) start_str = start_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 - if request.end_date > datetime.utcnow(): + if request.end_date > datetime.now(UTC): subscription.status = SubscriptionStatus.ACTIVE.value else: subscription.status = SubscriptionStatus.EXPIRED.value @@ -1107,7 +1107,7 @@ async def update_user_subscription( if request.action == 'cancel': subscription.status = SubscriptionStatus.EXPIRED.value - subscription.end_date = datetime.utcnow() + subscription.end_date = datetime.now(UTC) await db.commit() await db.refresh(subscription) @@ -1124,9 +1124,9 @@ async def update_user_subscription( if request.action == 'activate': 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 - subscription.end_date = datetime.utcnow() + timedelta(days=30) + subscription.end_date = datetime.now(UTC) + timedelta(days=30) await db.commit() await db.refresh(subscription) @@ -1196,7 +1196,7 @@ async def update_user_subscription( await db.delete(traffic_purchase) # Recalculate traffic_reset_at from remaining active purchases - now = datetime.utcnow() + now = datetime.now(UTC) remaining_query = select(TrafficPurchase).where( TrafficPurchase.subscription_id == subscription.id, TrafficPurchase.expires_at > now, @@ -1388,7 +1388,7 @@ async def update_user_status( ) user.status = new_status - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) await db.commit() await db.refresh(user) @@ -1456,7 +1456,7 @@ async def update_user_restrictions( if request.restriction_reason is not None: user.restriction_reason = request.restriction_reason - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) await db.commit() await db.refresh(user) @@ -1511,7 +1511,7 @@ async def update_user_promo_group( promo_group_name = promo_group.name user.promo_group_id = new_promo_group_id - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) await db.commit() await db.refresh(user) @@ -1552,7 +1552,7 @@ async def update_user_referral_commission( old_commission = user.referral_commission_percent user.referral_commission_percent = request.commission_percent - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) await db.commit() logger.info( @@ -1837,7 +1837,7 @@ async def reset_user_trial( # Reset trial flag user.has_used_trial = False - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) await db.commit() @@ -1907,7 +1907,7 @@ async def reset_user_subscription( await db.execute(delete(Subscription).where(Subscription.user_id == user_id)) subscription_deleted = True - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) await db.commit() reason_text = f' (reason: {request.reason})' if request.reason else '' @@ -1971,7 +1971,7 @@ async def disable_user( # Block user account user.status = UserStatus.BLOCKED.value - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) await db.commit() 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}') if bot_sub_end_date and panel_expire_at: - # Convert both to naive UTC for comparison - # Bot dates are stored as naive UTC - 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) + bot_end_utc = bot_sub_end_date if bot_sub_end_date.tzinfo else bot_sub_end_date + panel_end_utc = panel_datetime_to_utc(panel_expire_at) diff_seconds = abs((bot_end_utc - panel_end_utc).total_seconds()) # 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, username=panel_user.username, 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_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, @@ -2338,13 +2333,10 @@ async def sync_user_from_panel( # Update end date (normalize timezone) if panel_user.expire_at: - # Panel returns local time with misleading +00:00 offset - panel_expire_utc = panel_datetime_to_naive_utc(panel_user.expire_at) + panel_expire_utc = panel_datetime_to_utc(panel_user.expire_at) - sub_end_naive = ( - sub.end_date.replace(tzinfo=None) if sub.end_date and sub.end_date.tzinfo else sub.end_date - ) - if sub_end_naive != panel_expire_utc: + sub_end_utc = sub.end_date if sub.end_date and sub.end_date.tzinfo else sub.end_date + if sub_end_utc != panel_expire_utc: changes['end_date'] = { 'old': sub.end_date.isoformat() if sub.end_date else None, 'new': panel_expire_utc.isoformat(), @@ -2353,7 +2345,7 @@ async def sync_user_from_panel( # Update status 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 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: @@ -2410,9 +2402,8 @@ async def sync_user_from_panel( panel_traffic_limit = ( 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_naive = panel_datetime_to_naive_utc(panel_user.expire_at) - days_remaining = max(1, (panel_expire_naive - datetime.utcnow()).days) + panel_expire_utc = panel_datetime_to_utc(panel_user.expire_at) + days_remaining = max(1, (panel_expire_utc - datetime.now(UTC)).days) new_sub = await create_paid_subscription( db=db, @@ -2427,8 +2418,8 @@ async def sync_user_from_panel( changes['subscription_created'] = True # Update last sync time - user.last_remnawave_sync = datetime.utcnow() - user.updated_at = datetime.utcnow() + user.last_remnawave_sync = datetime.now(UTC) + user.updated_at = datetime.now(UTC) await db.commit() @@ -2502,14 +2493,14 @@ async def sync_user_to_panel( is_active = ( sub.status in (SubscriptionStatus.ACTIVE.value, SubscriptionStatus.TRIAL.value) 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 # Ensure expire_at is in future for panel expire_at = sub.end_date - if expire_at and expire_at <= datetime.utcnow(): - expire_at = datetime.utcnow() + timedelta(minutes=1) + if expire_at and expire_at <= datetime.now(UTC): + expire_at = datetime.now(UTC) + timedelta(minutes=1) username = settings.format_remnawave_username( full_name=user.full_name, @@ -2595,7 +2586,7 @@ async def sync_user_to_panel( # Create new user in panel create_kwargs = { '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, 'traffic_limit_bytes': traffic_limit_bytes, 'traffic_limit_strategy': TrafficLimitStrategy.MONTH, @@ -2620,8 +2611,8 @@ async def sync_user_to_panel( action = 'created' # Update last sync time - user.last_remnawave_sync = datetime.utcnow() - user.updated_at = datetime.utcnow() + user.last_remnawave_sync = datetime.now(UTC) + user.updated_at = datetime.now(UTC) await db.commit() diff --git a/app/cabinet/routes/auth.py b/app/cabinet/routes/auth.py index e09169fb..20c40309 100644 --- a/app/cabinet/routes/auth.py +++ b/app/cabinet/routes/auth.py @@ -25,7 +25,7 @@ from app.database.crud.user import ( from app.database.models import CabinetRefreshToken, User from app.services.disposable_email_service import disposable_email_service 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 ( 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) # 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_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 # 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: sub_status = SubscriptionStatus.ACTIVE @@ -313,7 +313,7 @@ async def auth_telegram( ) # Update last login - user.cabinet_last_login = datetime.utcnow() + user.cabinet_last_login = datetime.now(UTC) await db.commit() response = _create_auth_response(user) @@ -374,7 +374,7 @@ async def auth_telegram_widget( if request.last_name != user.last_name: user.last_name = request.last_name - user.cabinet_last_login = datetime.utcnow() + user.cabinet_last_login = datetime.now(UTC) await db.commit() response = _create_auth_response(user) @@ -546,7 +546,7 @@ async def register_email_standalone( # Для тестового email - автоматически верифицировать if is_test_email: user.email_verified = True - user.email_verified_at = datetime.utcnow() + user.email_verified_at = datetime.now(UTC) await db.commit() logger.info('Test email auto-verified: user_id', email=request.email, user_id=user.id) else: @@ -633,10 +633,10 @@ async def verify_email( # Mark email as verified 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_expires = None - user.cabinet_last_login = datetime.utcnow() + user.cabinet_last_login = datetime.now(UTC) await db.commit() @@ -750,7 +750,7 @@ async def login_email( language='ru', ) user.email_verified = True - user.email_verified_at = datetime.utcnow() + user.email_verified_at = datetime.now(UTC) await db.commit() else: raise HTTPException( @@ -783,7 +783,7 @@ async def login_email( detail='User account is not active', ) - user.cabinet_last_login = datetime.utcnow() + user.cabinet_last_login = datetime.now(UTC) await db.commit() response = _create_auth_response(user) @@ -871,7 +871,7 @@ async def logout( token_record = result.scalar_one_or_none() if token_record: - token_record.revoked_at = datetime.utcnow() + token_record.revoked_at = datetime.now(UTC) await db.commit() return {'message': 'Logged out successfully'} diff --git a/app/cabinet/routes/contests.py b/app/cabinet/routes/contests.py index 1600e555..01dc3266 100644 --- a/app/cabinet/routes/contests.py +++ b/app/cabinet/routes/contests.py @@ -1,7 +1,7 @@ """Contests routes for cabinet - user participation in games/contests.""" import random -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from typing import Any 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' 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.refresh(subscription) diff --git a/app/cabinet/routes/notifications.py b/app/cabinet/routes/notifications.py index 6da075a0..1fd8015e 100644 --- a/app/cabinet/routes/notifications.py +++ b/app/cabinet/routes/notifications.py @@ -1,6 +1,6 @@ """Notification settings routes for cabinet.""" -from datetime import datetime +from datetime import UTC, datetime from typing import Any import structlog @@ -112,7 +112,7 @@ async def update_notification_settings( user.notification_settings = {} user.notification_settings = new_settings - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) await db.commit() await db.refresh(user) diff --git a/app/cabinet/routes/oauth.py b/app/cabinet/routes/oauth.py index 46f3e1c0..9142a3df 100644 --- a/app/cabinet/routes/oauth.py +++ b/app/cabinet/routes/oauth.py @@ -34,7 +34,7 @@ router = APIRouter(prefix='/auth/oauth', tags=['Cabinet OAuth']) async def _finalize_oauth_login(db: AsyncSession, user: User, provider: str) -> AuthResponse: """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() auth_response = _create_auth_response(user) await _store_refresh_token(db, user.id, auth_response.refresh_token, device_info=f'oauth:{provider}') diff --git a/app/cabinet/routes/polls.py b/app/cabinet/routes/polls.py index c66e9ae3..30f623c1 100644 --- a/app/cabinet/routes/polls.py +++ b/app/cabinet/routes/polls.py @@ -1,6 +1,6 @@ """Polls routes for cabinet - user participation in polls/surveys.""" -from datetime import datetime +from datetime import UTC, datetime import structlog from fastapi import APIRouter, Depends, HTTPException, status @@ -247,7 +247,7 @@ async def start_poll( # Mark as started if not already if not response.started_at: - response.started_at = datetime.utcnow() + response.started_at = datetime.now(UTC) await db.commit() # Get next unanswered question @@ -346,7 +346,7 @@ async def answer_question( ) # Poll completed - response.completed_at = datetime.utcnow() + response.completed_at = datetime.now(UTC) await db.commit() # Award reward if any diff --git a/app/cabinet/routes/promo.py b/app/cabinet/routes/promo.py index d3ca3994..cb46e47c 100644 --- a/app/cabinet/routes/promo.py +++ b/app/cabinet/routes/promo.py @@ -1,6 +1,6 @@ """Promo offers routes for cabinet - personal discounts and offers.""" -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from typing import Any import structlog @@ -112,7 +112,7 @@ async def get_promo_offers( db: AsyncSession = Depends(get_cabinet_db), ): """Get list of available promo offers for the user.""" - now = datetime.utcnow() + now = datetime.now(UTC) result = await db.execute( select(DiscountOffer) @@ -151,7 +151,7 @@ async def get_active_discount( expires_at = user.promo_offer_discount_expires_at 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) return ActiveDiscountInfo( @@ -284,7 +284,7 @@ async def claim_promo_offer( detail='Offer not found', ) - now = datetime.utcnow() + now = datetime.now(UTC) if offer.claimed_at is not None: raise HTTPException( @@ -408,7 +408,7 @@ async def clear_active_discount( user.promo_offer_discount_percent = 0 user.promo_offer_discount_source = None user.promo_offer_discount_expires_at = None - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) await db.commit() diff --git a/app/cabinet/routes/subscription.py b/app/cabinet/routes/subscription.py index 7655fa79..b4db6deb 100644 --- a/app/cabinet/routes/subscription.py +++ b/app/cabinet/routes/subscription.py @@ -135,7 +135,7 @@ def _subscription_to_response( traffic_purchases: list[dict[str, Any]] | None = None, ) -> SubscriptionData: """Convert Subscription model to response.""" - now = datetime.utcnow() + now = datetime.now(UTC) # Use actual_status property for correct status (same as bot uses) actual_status = subscription.actual_status @@ -276,7 +276,7 @@ async def get_subscription( traffic_purchases_data = [] from app.database.models import TrafficPurchase - now = datetime.utcnow() + now = datetime.now(UTC) purchases_query = ( select(TrafficPurchase) .where(TrafficPurchase.subscription_id == fresh_user.subscription.id) @@ -515,7 +515,7 @@ async def renew_subscription( user.promo_offer_discount_expires_at = None # 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: user.subscription.end_date = user.subscription.end_date + timedelta(days=request.period_days) else: @@ -832,9 +832,9 @@ async def purchase_traffic( # Устанавливаем дату сброса трафика (только при первой докупке) # При повторной докупке дата НЕ продлевается 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( 'Set traffic_reset_at for subscription', subscription_id=subscription.id, @@ -1137,7 +1137,7 @@ async def get_trial_info( # Check if user already has an active subscription if user.subscription: - now = datetime.utcnow() + now = datetime.now(UTC) is_active = ( 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 if user.subscription: - now = datetime.utcnow() + now = datetime.now(UTC) is_active = ( 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 if is_daily_tariff: - subscription.last_daily_charge_at = datetime.utcnow() + subscription.last_daily_charge_at = datetime.now(UTC) subscription.is_daily_paused = False await db.commit() await db.refresh(subscription) @@ -2088,7 +2088,7 @@ async def purchase_tariff( try: # Determine if this is a new subscription or extension 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 = ( NotificationType.SUBSCRIPTION_ACTIVATED @@ -2124,7 +2124,7 @@ async def purchase_tariff( notification_service = AdminNotificationService(bot) # Определяем тип покупки: новая подписка или продление 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( db=db, @@ -2211,7 +2211,7 @@ async def purchase_devices( ) # Calculate prorated price based on remaining days - from datetime import datetime + from datetime import UTC, datetime now = datetime.now(UTC) end_date = subscription.end_date @@ -2627,7 +2627,7 @@ async def get_device_price( } # Calculate prorated price - from datetime import datetime + from datetime import UTC, datetime now = datetime.now(UTC) end_date = subscription.end_date @@ -3079,9 +3079,9 @@ async def get_available_countries( connected_squads = user.subscription.connected_squads or [] # Calculate days left for prorated pricing 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) # Get discount from promo group @@ -3263,7 +3263,7 @@ async def update_countries( # Update connected squads user.subscription.connected_squads = selected_countries - user.subscription.updated_at = datetime.utcnow() + user.subscription.updated_at = datetime.now(UTC) await db.commit() # Sync with RemnaWave @@ -3910,7 +3910,7 @@ async def reduce_devices( # Update subscription subscription.device_limit = new_device_limit - subscription.updated_at = datetime.utcnow() + subscription.updated_at = datetime.now(UTC) await db.commit() # Update RemnaWave @@ -4010,8 +4010,8 @@ async def preview_tariff_switch( # Calculate remaining days remaining_days = 0 - if user.subscription.end_date and user.subscription.end_date > datetime.utcnow(): - delta = 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.now(UTC) remaining_days = max(0, delta.days) # Calculate switch cost @@ -4128,7 +4128,7 @@ async def switch_tariff( db: AsyncSession = Depends(get_cabinet_db), ) -> dict[str, Any]: """Switch to a different tariff without changing end date.""" - from datetime import timedelta + from datetime import UTC, timedelta if not settings.is_tariffs_mode(): raise HTTPException( @@ -4195,8 +4195,8 @@ async def switch_tariff( # Calculate remaining days remaining_days = 0 - if user.subscription.end_date and user.subscription.end_date > datetime.utcnow(): - delta = 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.now(UTC) remaining_days = max(0, delta.days) # Calculate cost @@ -4334,14 +4334,14 @@ async def switch_tariff( if switching_to_daily: # 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.last_daily_charge_at = datetime.utcnow() + user.subscription.end_date = datetime.now(UTC) + timedelta(days=1) + user.subscription.last_daily_charge_at = datetime.now(UTC) user.subscription.is_daily_paused = False 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.updated_at = datetime.utcnow() + user.subscription.updated_at = datetime.now(UTC) await db.commit() # Sync with RemnaWave @@ -4425,7 +4425,7 @@ async def toggle_subscription_pause( db: AsyncSession = Depends(get_cabinet_db), ) -> dict[str, Any]: """Toggle pause/resume for daily subscription.""" - from datetime import timedelta + from datetime import UTC, timedelta await db.refresh(user, ['subscription']) @@ -4476,8 +4476,8 @@ async def toggle_subscription_pause( # Restore ACTIVE status if was DISABLED if was_disabled: user.subscription.status = SubscriptionStatus.ACTIVE.value - user.subscription.last_daily_charge_at = datetime.utcnow() - user.subscription.end_date = datetime.utcnow() + timedelta(days=1) + user.subscription.last_daily_charge_at = datetime.now(UTC) + user.subscription.end_date = datetime.now(UTC) + timedelta(days=1) await db.commit() await db.refresh(user.subscription) @@ -4614,7 +4614,7 @@ async def switch_traffic_package( user.subscription.traffic_limit_gb = new_traffic user.subscription.purchased_traffic_gb = 0 # Reset purchased traffic on switch 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() # Sync with RemnaWave @@ -4733,7 +4733,7 @@ async def refresh_traffic( used_gb = traffic_stats.get('used_traffic_gb', 0) if abs((user.subscription.traffic_used_gb or 0) - used_gb) > 0.01: user.subscription.traffic_used_gb = used_gb - user.subscription.updated_at = datetime.utcnow() + user.subscription.updated_at = datetime.now(UTC) await db.commit() # Calculate percentage diff --git a/app/cabinet/routes/tickets.py b/app/cabinet/routes/tickets.py index 9f96650e..87d7f780 100644 --- a/app/cabinet/routes/tickets.py +++ b/app/cabinet/routes/tickets.py @@ -1,7 +1,7 @@ """Support tickets routes for cabinet.""" import math -from datetime import datetime +from datetime import UTC, datetime import structlog from fastapi import APIRouter, Depends, HTTPException, Query, status @@ -137,8 +137,8 @@ async def create_ticket( title=request.title, status='open', priority='normal', - created_at=datetime.utcnow(), - updated_at=datetime.utcnow(), + created_at=datetime.now(UTC), + updated_at=datetime.now(UTC), ) db.add(ticket) await db.flush() @@ -152,7 +152,7 @@ async def create_ticket( media_type=request.media_type, media_file_id=request.media_file_id, media_caption=request.media_caption, - created_at=datetime.utcnow(), + created_at=datetime.now(UTC), ) db.add(message) await db.commit() @@ -268,14 +268,14 @@ async def add_ticket_message( media_type=request.media_type, media_file_id=request.media_file_id, media_caption=request.media_caption, - created_at=datetime.utcnow(), + created_at=datetime.now(UTC), ) db.add(message) # Update ticket status and timestamp if ticket.status == 'answered': ticket.status = 'pending' - ticket.updated_at = datetime.utcnow() + ticket.updated_at = datetime.now(UTC) await db.commit() await db.refresh(message) diff --git a/app/cabinet/services/email_template_overrides.py b/app/cabinet/services/email_template_overrides.py index f8bfae09..de4833e5 100644 --- a/app/cabinet/services/email_template_overrides.py +++ b/app/cabinet/services/email_template_overrides.py @@ -4,7 +4,7 @@ Service for managing email template overrides stored in the database. Custom templates override the hardcoded defaults from email_templates.py. """ -from datetime import datetime +from datetime import UTC, datetime from typing import Any import structlog @@ -124,7 +124,7 @@ async def save_template_override( ) row = existing.fetchone() - now = datetime.utcnow() + now = datetime.now(UTC) if row: # Update diff --git a/app/database/crud/campaign.py b/app/database/crud/campaign.py index 07d1108f..2c3717b8 100644 --- a/app/database/crud/campaign.py +++ b/app/database/crud/campaign.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import UTC, datetime import structlog from sqlalchemy import and_, delete, func, select, update @@ -151,7 +151,7 @@ async def update_campaign( if not update_data: 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.commit() diff --git a/app/database/crud/cloudpayments.py b/app/database/crud/cloudpayments.py index 0ca32e90..1b03a84f 100644 --- a/app/database/crud/cloudpayments.py +++ b/app/database/crud/cloudpayments.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import datetime +from datetime import UTC, datetime from typing import Any import structlog @@ -127,7 +127,7 @@ async def update_cloudpayments_payment( if hasattr(payment, key): setattr(payment, key, value) - payment.updated_at = datetime.utcnow() + payment.updated_at = datetime.now(UTC) await db.flush() await db.refresh(payment) @@ -171,7 +171,7 @@ async def mark_cloudpayments_payment_as_paid( payment.status = 'completed' payment.is_paid = True - payment.paid_at = datetime.utcnow() + payment.paid_at = datetime.now(UTC) if transaction_id_cp is not None: payment.transaction_id_cp = transaction_id_cp @@ -190,7 +190,7 @@ async def mark_cloudpayments_payment_as_paid( if callback_payload: payment.callback_payload = callback_payload - payment.updated_at = datetime.utcnow() + payment.updated_at = datetime.now(UTC) await db.flush() await db.refresh(payment) diff --git a/app/database/crud/contest.py b/app/database/crud/contest.py index 5c100b2d..43f00471 100644 --- a/app/database/crud/contest.py +++ b/app/database/crud/contest.py @@ -1,5 +1,5 @@ from collections.abc import Sequence -from datetime import datetime +from datetime import UTC, datetime import structlog from sqlalchemy import and_, delete, desc, select @@ -107,7 +107,7 @@ async def create_round( async def get_active_rounds(db: AsyncSession) -> list[ContestRound]: - now = datetime.utcnow() + now = datetime.now(UTC) result = await db.execute( select(ContestRound) .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: - now = datetime.utcnow() + now = datetime.now(UTC) result = await db.execute( select(ContestRound) .options(selectinload(ContestRound.template)) diff --git a/app/database/crud/cryptobot.py b/app/database/crud/cryptobot.py index 063891b6..98a94365 100644 --- a/app/database/crud/cryptobot.py +++ b/app/database/crud/cryptobot.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import UTC, datetime import structlog from sqlalchemy import and_, select @@ -76,7 +76,7 @@ async def update_cryptobot_payment_status( return None payment.status = status - payment.updated_at = datetime.utcnow() + payment.updated_at = datetime.now(UTC) if status == 'paid' and paid_at: payment.paid_at = paid_at @@ -97,7 +97,7 @@ async def link_cryptobot_payment_to_transaction( return None payment.transaction_id = transaction_id - payment.updated_at = datetime.utcnow() + payment.updated_at = datetime.now(UTC) await db.commit() 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]: - 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( select(CryptoBotPayment) diff --git a/app/database/crud/discount_offer.py b/app/database/crud/discount_offer.py index 91de782e..412d8807 100644 --- a/app/database/crud/discount_offer.py +++ b/app/database/crud/discount_offer.py @@ -1,6 +1,6 @@ from __future__ import annotations -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta import structlog from sqlalchemy import func, select @@ -28,7 +28,7 @@ async def upsert_discount_offer( ) -> DiscountOffer: """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( select(DiscountOffer) @@ -116,7 +116,7 @@ async def list_active_discount_offers_for_user( ) -> list[DiscountOffer]: """Return active (not yet claimed) offers for a user.""" - now = datetime.utcnow() + now = datetime.now(UTC) stmt = ( select(DiscountOffer) .options( @@ -161,7 +161,7 @@ async def mark_offer_claimed( *, details: dict | None = None, ) -> DiscountOffer: - offer.claimed_at = datetime.utcnow() + offer.claimed_at = datetime.now(UTC) offer.is_active = False await db.commit() await db.refresh(offer) @@ -190,7 +190,7 @@ async def mark_offer_claimed( async def deactivate_expired_offers(db: AsyncSession) -> int: - now = datetime.utcnow() + now = datetime.now(UTC) result = await db.execute( select(DiscountOffer).where( DiscountOffer.is_active == True, diff --git a/app/database/crud/faq.py b/app/database/crud/faq.py index 84ccb371..7bc4c063 100644 --- a/app/database/crud/faq.py +++ b/app/database/crud/faq.py @@ -1,5 +1,5 @@ from collections.abc import Iterable -from datetime import datetime +from datetime import UTC, datetime import structlog 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: setting.is_enabled = bool(enabled) - setting.updated_at = datetime.utcnow() + setting.updated_at = datetime.now(UTC) else: setting = FaqSetting( language=language, @@ -117,7 +117,7 @@ async def update_faq_page( if is_active is not None: page.is_active = bool(is_active) - page.updated_at = datetime.utcnow() + page.updated_at = datetime.now(UTC) await db.commit() await db.refresh(page) @@ -139,6 +139,6 @@ async def bulk_update_order( ) -> None: for page_id, order in pages: 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() diff --git a/app/database/crud/freekassa.py b/app/database/crud/freekassa.py index 31ba2b73..b6ed4a07 100644 --- a/app/database/crud/freekassa.py +++ b/app/database/crud/freekassa.py @@ -1,7 +1,7 @@ """CRUD операции для платежей Freekassa.""" import json -from datetime import datetime +from datetime import UTC, datetime import structlog from sqlalchemy import select @@ -77,10 +77,10 @@ async def update_freekassa_payment_status( """Обновляет статус платежа.""" payment.status = status payment.is_paid = is_paid - payment.updated_at = datetime.utcnow() + payment.updated_at = datetime.now(UTC) if is_paid: - payment.paid_at = datetime.utcnow() + payment.paid_at = datetime.now(UTC) if freekassa_order_id: payment.freekassa_order_id = freekassa_order_id if payment_system_id is not None: @@ -134,7 +134,7 @@ async def get_expired_pending_payments( db: AsyncSession, ) -> list[FreekassaPayment]: """Получает просроченные платежи в статусе pending.""" - now = datetime.utcnow() + now = datetime.now(UTC) result = await db.execute( select(FreekassaPayment).where( FreekassaPayment.status == 'pending', diff --git a/app/database/crud/heleket.py b/app/database/crud/heleket.py index 19e7f34a..3889c1a9 100644 --- a/app/database/crud/heleket.py +++ b/app/database/crud/heleket.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import UTC, datetime from typing import Any import structlog @@ -129,7 +129,7 @@ async def update_heleket_payment( if paid_at is not None: payment.paid_at = paid_at - payment.updated_at = datetime.utcnow() + payment.updated_at = datetime.now(UTC) await db.commit() await db.refresh(payment) @@ -157,7 +157,7 @@ async def link_heleket_payment_to_transaction( return None payment.transaction_id = transaction_id - payment.updated_at = datetime.utcnow() + payment.updated_at = datetime.now(UTC) await db.commit() await db.refresh(payment) diff --git a/app/database/crud/kassa_ai.py b/app/database/crud/kassa_ai.py index 9b8cae01..16225c2a 100644 --- a/app/database/crud/kassa_ai.py +++ b/app/database/crud/kassa_ai.py @@ -1,7 +1,7 @@ """CRUD операции для платежей KassaAI.""" import json -from datetime import datetime +from datetime import UTC, datetime import structlog from sqlalchemy import select @@ -79,10 +79,10 @@ async def update_kassa_ai_payment_status( """Обновляет статус платежа.""" payment.status = status payment.is_paid = is_paid - payment.updated_at = datetime.utcnow() + payment.updated_at = datetime.now(UTC) if is_paid: - payment.paid_at = datetime.utcnow() + payment.paid_at = datetime.now(UTC) if kassa_ai_order_id: payment.kassa_ai_order_id = kassa_ai_order_id if payment_system_id is not None: @@ -136,7 +136,7 @@ async def get_expired_pending_kassa_ai_payments( db: AsyncSession, ) -> list[KassaAiPayment]: """Получает просроченные платежи в статусе pending.""" - now = datetime.utcnow() + now = datetime.now(UTC) result = await db.execute( select(KassaAiPayment).where( KassaAiPayment.status == 'pending', diff --git a/app/database/crud/mulenpay.py b/app/database/crud/mulenpay.py index 44fe1e46..97dbc3a8 100644 --- a/app/database/crud/mulenpay.py +++ b/app/database/crud/mulenpay.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import UTC, datetime import structlog from sqlalchemy import select @@ -90,7 +90,7 @@ async def update_mulenpay_payment_status( if metadata is not None: payment.metadata_json = metadata - payment.updated_at = datetime.utcnow() + payment.updated_at = datetime.now(UTC) await db.commit() await db.refresh(payment) return payment @@ -103,7 +103,7 @@ async def update_mulenpay_payment_metadata( metadata: dict, ) -> MulenPayPayment: payment.metadata_json = metadata - payment.updated_at = datetime.utcnow() + payment.updated_at = datetime.now(UTC) await db.commit() await db.refresh(payment) return payment @@ -116,7 +116,7 @@ async def link_mulenpay_payment_to_transaction( transaction_id: int, ) -> MulenPayPayment: payment.transaction_id = transaction_id - payment.updated_at = datetime.utcnow() + payment.updated_at = datetime.now(UTC) await db.commit() await db.refresh(payment) return payment diff --git a/app/database/crud/platega.py b/app/database/crud/platega.py index 70172e83..6d676f9c 100644 --- a/app/database/crud/platega.py +++ b/app/database/crud/platega.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import datetime +from datetime import UTC, datetime from typing import Any import structlog @@ -115,7 +115,7 @@ async def update_platega_payment( if expires_at is not None: payment.expires_at = expires_at - payment.updated_at = datetime.utcnow() + payment.updated_at = datetime.now(UTC) await db.commit() await db.refresh(payment) @@ -129,7 +129,7 @@ async def link_platega_payment_to_transaction( transaction_id: int, ) -> PlategaPayment: payment.transaction_id = transaction_id - payment.updated_at = datetime.utcnow() + payment.updated_at = datetime.now(UTC) await db.commit() await db.refresh(payment) return payment diff --git a/app/database/crud/privacy_policy.py b/app/database/crud/privacy_policy.py index b9e68abd..11972494 100644 --- a/app/database/crud/privacy_policy.py +++ b/app/database/crud/privacy_policy.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import UTC, datetime import structlog from sqlalchemy import select @@ -26,7 +26,7 @@ async def upsert_privacy_policy( if policy: policy.content = content or '' - policy.updated_at = datetime.utcnow() + policy.updated_at = datetime.now(UTC) else: policy = PrivacyPolicy( language=language, @@ -52,7 +52,7 @@ async def set_privacy_policy_enabled( if policy: policy.is_enabled = bool(enabled) - policy.updated_at = datetime.utcnow() + policy.updated_at = datetime.now(UTC) else: policy = PrivacyPolicy( language=language, diff --git a/app/database/crud/promo_offer_template.py b/app/database/crud/promo_offer_template.py index 0f1deab7..b600d400 100644 --- a/app/database/crud/promo_offer_template.py +++ b/app/database/crud/promo_offer_template.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections.abc import Iterable -from datetime import datetime +from datetime import UTC, datetime from sqlalchemy import select 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: existing.message_text = new_message - existing.updated_at = datetime.utcnow() + existing.updated_at = datetime.now(UTC) await db.flush() 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: existing.active_discount_hours = target_active_hours - existing.updated_at = datetime.utcnow() + existing.updated_at = datetime.now(UTC) await db.flush() templates.append(existing) continue @@ -204,7 +204,7 @@ async def update_promo_offer_template( if is_active is not None: template.is_active = is_active - template.updated_at = datetime.utcnow() + template.updated_at = datetime.now(UTC) await db.commit() await db.refresh(template) diff --git a/app/database/crud/promocode.py b/app/database/crud/promocode.py index ca51b1fd..fe4e9fa9 100644 --- a/app/database/crud/promocode.py +++ b/app/database/crud/promocode.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import UTC, datetime import structlog 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: - 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) 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: """Подсчитывает количество активаций промокодов пользователем за последние 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( 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): setattr(promocode, field, value) - promocode.updated_at = datetime.utcnow() + promocode.updated_at = datetime.now(UTC) await db.commit() 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() - today = datetime.utcnow().date() + today = datetime.now(UTC).date() today_uses_result = await db.execute( select(func.count(PromoCodeUse.id)).where( and_(PromoCodeUse.promocode_id == promocode_id, PromoCodeUse.used_at >= today) diff --git a/app/database/crud/public_offer.py b/app/database/crud/public_offer.py index 8ae9f201..19d4f169 100644 --- a/app/database/crud/public_offer.py +++ b/app/database/crud/public_offer.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import UTC, datetime import structlog from sqlalchemy import select @@ -26,7 +26,7 @@ async def upsert_public_offer( if offer: offer.content = content or '' - offer.updated_at = datetime.utcnow() + offer.updated_at = datetime.now(UTC) else: offer = PublicOffer( language=language, @@ -52,7 +52,7 @@ async def set_public_offer_enabled( if offer: offer.is_enabled = bool(enabled) - offer.updated_at = datetime.utcnow() + offer.updated_at = datetime.now(UTC) else: offer = PublicOffer( language=language, diff --git a/app/database/crud/referral.py b/app/database/crud/referral.py index 61aa0d33..180e81e7 100644 --- a/app/database/crud/referral.py +++ b/app/database/crud/referral.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta import structlog 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( 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() - week_ago = datetime.utcnow() - timedelta(days=7) + week_ago = datetime.now(UTC) - timedelta(days=7) week_referral_earnings_result = await db.execute( 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() - month_ago = datetime.utcnow() - timedelta(days=30) + month_ago = datetime.now(UTC) - timedelta(days=30) month_referral_earnings_result = await db.execute( 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 - now = datetime.utcnow() + now = datetime.now(UTC) if period == 'week': start_date = now - timedelta(days=7) 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) - 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) from app.database.models import Subscription, SubscriptionStatus - current_time = datetime.utcnow() + current_time = datetime.now(UTC) active_referrals_result = await db.execute( select(func.count(User.id)) diff --git a/app/database/crud/referral_contest.py b/app/database/crud/referral_contest.py index 698591a5..7629f813 100644 --- a/app/database/crud/referral_contest.py +++ b/app/database/crud/referral_contest.py @@ -1,5 +1,5 @@ from collections.abc import Sequence -from datetime import date, datetime, time +from datetime import UTC, date, datetime, time import structlog from sqlalchemy import and_, desc, func, select @@ -165,7 +165,7 @@ async def add_contest_event( referral_id=referral_id, amount_kopeks=amount_kopeks, event_type=event_type, - occurred_at=datetime.utcnow(), + occurred_at=datetime.now(UTC), ) db.add(event) await db.commit() @@ -512,7 +512,7 @@ async def upsert_contest_event( referral_id=referral_id, amount_kopeks=amount_kopeks, event_type=event_type, - occurred_at=datetime.utcnow(), + occurred_at=datetime.now(UTC), ) db.add(event) await db.commit() diff --git a/app/database/crud/rules.py b/app/database/crud/rules.py index 729f70a8..2462b8ef 100644 --- a/app/database/crud/rules.py +++ b/app/database/crud/rules.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import UTC, datetime import structlog from sqlalchemy import select, update @@ -30,7 +30,7 @@ async def create_or_update_rules( for rule in existing_rules: 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) @@ -47,7 +47,7 @@ async def clear_all_rules(db: AsyncSession, language: str = 'ru') -> bool: result = await db.execute( update(ServiceRule) .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() @@ -110,7 +110,7 @@ async def restore_rules_version(db: AsyncSession, rule_id: int, language: str = await db.execute( update(ServiceRule) .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( diff --git a/app/database/crud/server_squad.py b/app/database/crud/server_squad.py index ec3daff5..66ebc177 100644 --- a/app/database/crud/server_squad.py +++ b/app/database/crud/server_squad.py @@ -1,6 +1,6 @@ import random from collections.abc import Iterable, Sequence -from datetime import datetime +from datetime import UTC, datetime import structlog 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): subscription.connected_squads = filtered_squads - subscription.updated_at = datetime.utcnow() + subscription.updated_at = datetime.now(UTC) cleaned_subscriptions += 1 # 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] if len(filtered) != len(current): tariff.allowed_squads = filtered - tariff.updated_at = datetime.utcnow() + tariff.updated_at = datetime.now(UTC) cleaned_tariffs += 1 logger.info( '🧹 Тариф "%s" (ID: %s): удалены несуществующие сквады %s', diff --git a/app/database/crud/subscription.py b/app/database/crud/subscription.py index 20078348..d139dc83 100644 --- a/app/database/crud/subscription.py +++ b/app/database/crud/subscription.py @@ -31,7 +31,7 @@ def is_recently_updated_by_webhook(subscription: Subscription) -> bool: """Return True if subscription was updated by webhook within guard window.""" if not subscription.last_webhook_update_at: 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 @@ -101,13 +101,13 @@ async def create_trial_subscription( except Exception as 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) existing = await get_subscription_by_user_id(db, user_id) if existing and existing.is_trial and existing.status == SubscriptionStatus.PENDING.value: existing.status = SubscriptionStatus.ACTIVE.value - existing.start_date = datetime.utcnow() + existing.start_date = datetime.now(UTC) existing.end_date = end_date existing.traffic_limit_gb = traffic_limit_gb existing.device_limit = device_limit @@ -124,7 +124,7 @@ async def create_trial_subscription( user_id=user_id, status=SubscriptionStatus.ACTIVE.value, is_trial=True, - start_date=datetime.utcnow(), + start_date=datetime.now(UTC), end_date=end_date, traffic_limit_gb=traffic_limit_gb, device_limit=device_limit, @@ -176,7 +176,7 @@ async def create_paid_subscription( is_trial: bool = False, tariff_id: int | None = None, ) -> Subscription: - end_date = datetime.utcnow() + timedelta(days=duration_days) + end_date = datetime.now(UTC) + timedelta(days=duration_days) if device_limit is None: device_limit = settings.DEFAULT_DEVICE_LIMIT @@ -185,7 +185,7 @@ async def create_paid_subscription( user_id=user_id, status=SubscriptionStatus.ACTIVE.value, is_trial=is_trial, - start_date=datetime.utcnow(), + start_date=datetime.now(UTC), end_date=end_date, traffic_limit_gb=traffic_limit_gb, device_limit=device_limit, @@ -253,7 +253,7 @@ async def replace_subscription( ) -> Subscription: """Перезаписывает параметры существующей подписки пользователя.""" - current_time = datetime.utcnow() + current_time = datetime.now(UTC) old_squads = set(subscription.connected_squads or []) new_squads = set(connected_squads or []) @@ -341,7 +341,7 @@ async def extend_subscription( device_limit: Лимит устройств (опционально, для режима тарифов) connected_squads: Список UUID сквадов (опционально, для режима тарифов) """ - current_time = datetime.utcnow() + current_time = datetime.now(UTC) logger.info('🔄 Продление подписки на дней', subscription_id=subscription.id, days=days) logger.info( @@ -535,16 +535,16 @@ async def extend_subscription( async def add_subscription_traffic(db: AsyncSession, subscription: Subscription, gb: int) -> Subscription: subscription.add_traffic(gb) - subscription.updated_at = datetime.utcnow() + subscription.updated_at = datetime.now(UTC) # Создаём новую запись докупки с индивидуальной датой истечения (30 дней) - from datetime import timedelta + from datetime import UTC, timedelta from sqlalchemy import select as sql_select 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) db.add(new_purchase) @@ -553,7 +553,7 @@ async def add_subscription_traffic(db: AsyncSession, subscription: Subscription, subscription.purchased_traffic_gb = current_purchased + gb # Устанавливаем traffic_reset_at на ближайшую дату истечения из всех активных докупок - now = datetime.utcnow() + now = datetime.now(UTC) active_purchases_query = ( sql_select(TrafficPurchase) .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: subscription.device_limit += devices - subscription.updated_at = datetime.utcnow() + subscription.updated_at = datetime.now(UTC) await db.commit() 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: if squad_uuid not in subscription.connected_squads: subscription.connected_squads = subscription.connected_squads + [squad_uuid] - subscription.updated_at = datetime.utcnow() + subscription.updated_at = datetime.now(UTC) await db.commit() await db.refresh(subscription) @@ -612,7 +612,7 @@ async def remove_subscription_squad(db: AsyncSession, subscription: Subscription squads = subscription.connected_squads.copy() squads.remove(squad_uuid) subscription.connected_squads = squads - subscription.updated_at = datetime.utcnow() + subscription.updated_at = datetime.now(UTC) await db.commit() await db.refresh(subscription) @@ -688,7 +688,7 @@ async def update_subscription_autopay( ) -> Subscription: subscription.autopay_enabled = enabled subscription.autopay_days_before = days_before - subscription.updated_at = datetime.utcnow() + subscription.updated_at = datetime.now(UTC) await db.commit() await db.refresh(subscription) @@ -700,7 +700,7 @@ async def update_subscription_autopay( async def deactivate_subscription(db: AsyncSession, subscription: Subscription) -> Subscription: subscription.status = SubscriptionStatus.DISABLED.value - subscription.updated_at = datetime.utcnow() + subscription.updated_at = datetime.now(UTC) await db.commit() await db.refresh(subscription) @@ -715,7 +715,7 @@ async def reactivate_subscription(db: AsyncSession, subscription: Subscription) Активирует только если подписка была DISABLED и ещё не истекла. Не логирует если реактивация не требуется. """ - now = datetime.utcnow() + now = datetime.now(UTC) # Тихо выходим если реактивация не нужна 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]: - threshold_date = datetime.utcnow() + timedelta(days=days_before) + threshold_date = datetime.now(UTC) + timedelta(days=days_before) result = await db.execute( select(Subscription) @@ -743,7 +743,7 @@ async def get_expiring_subscriptions(db: AsyncSession, days_before: int = 3) -> and_( Subscription.status == SubscriptionStatus.ACTIVE.value, 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( select(Subscription) .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() async def get_subscriptions_for_autopay(db: AsyncSession) -> list[Subscription]: - current_time = datetime.utcnow() + current_time = datetime.now(UTC) result = await db.execute( select(Subscription) @@ -811,7 +811,7 @@ async def get_subscriptions_statistics(db: AsyncSession) -> dict: paid_subscriptions = active_subscriptions - trial_subscriptions - today = datetime.utcnow().date() + today = datetime.now(UTC).date() today_result = await db.execute( select(func.count(Subscription.id)).where( 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() - week_ago = datetime.utcnow() - timedelta(days=7) + week_ago = datetime.now(UTC) - timedelta(days=7) week_result = await db.execute( select(func.count(Subscription.id)).where( 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() - month_ago = datetime.utcnow() - timedelta(days=30) + month_ago = datetime.now(UTC) - timedelta(days=30) month_result = await db.execute( select(func.count(Subscription.id)).where( 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: - 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 = 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: - now = datetime.utcnow() + now = datetime.now(UTC) result = await db.execute( 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: subscription.traffic_used_gb = used_gb - subscription.updated_at = datetime.utcnow() + subscription.updated_at = datetime.now(UTC) await db.commit() 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: subscription.status = SubscriptionStatus.EXPIRED.value - subscription.updated_at = datetime.utcnow() + subscription.updated_at = datetime.now(UTC) await db.commit() 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: - current_time = datetime.utcnow() + current_time = datetime.now(UTC) logger.info( '🔍 Проверка статуса подписки , текущий статус дата окончания текущее время', @@ -1595,7 +1595,7 @@ async def create_subscription_no_commit( """ 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: connected_squads = [] @@ -1645,7 +1645,7 @@ async def create_subscription( autopay_days_before: int | None = None, ) -> Subscription: 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: connected_squads = [] @@ -1693,7 +1693,7 @@ async def create_pending_subscription( is_trial: If True, marks the subscription as a trial subscription. """ trial_label = 'триальная ' if is_trial else '' - current_time = datetime.utcnow() + current_time = datetime.now(UTC) end_date = current_time + timedelta(days=duration_days) 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 - current_time = datetime.utcnow() + current_time = datetime.now(UTC) pending_subscription.status = SubscriptionStatus.ACTIVE.value # Если указан период, обновляем дату окончания @@ -1870,7 +1870,7 @@ async def activate_pending_trial_subscription( ) # Обновляем статус подписки на ACTIVE - current_time = datetime.utcnow() + current_time = datetime.now(UTC) 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 - now = datetime.utcnow() + now = datetime.now(UTC) one_day_ago = now - timedelta(hours=24) query = ( @@ -2020,8 +2020,8 @@ async def resume_daily_subscription( previous_status = subscription.status subscription.status = SubscriptionStatus.ACTIVE.value # Обновляем время последнего списания для корректного расчёта следующего - subscription.last_daily_charge_at = datetime.utcnow() - subscription.end_date = datetime.utcnow() + timedelta(days=1) + subscription.last_daily_charge_at = datetime.now(UTC) + subscription.end_date = datetime.now(UTC) + timedelta(days=1) logger.info( '✅ Суточная подписка восстановлена из в ACTIVE', subscription_id=subscription.id, @@ -2044,7 +2044,7 @@ async def update_daily_charge_time( charge_time: datetime = None, ) -> Subscription: """Обновляет время последнего суточного списания и продлевает подписку на 1 день.""" - now = charge_time or datetime.utcnow() + now = charge_time or datetime.now(UTC) subscription.last_daily_charge_at = now # Продлеваем подписку на 1 день от текущего момента diff --git a/app/database/crud/subscription_conversion.py b/app/database/crud/subscription_conversion.py index 187c7a92..50d3c3f9 100644 --- a/app/database/crud/subscription_conversion.py +++ b/app/database/crud/subscription_conversion.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta import structlog from sqlalchemy import func, select @@ -20,7 +20,7 @@ async def create_subscription_conversion( ) -> SubscriptionConversion: conversion = SubscriptionConversion( user_id=user_id, - converted_at=datetime.utcnow(), + converted_at=datetime.now(UTC), trial_duration_days=trial_duration_days, payment_method=payment_method, 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 = 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( select(func.count(SubscriptionConversion.id)).where(SubscriptionConversion.converted_at >= month_ago) ) diff --git a/app/database/crud/subscription_event.py b/app/database/crud/subscription_event.py index 63cb02cd..f467fbb0 100644 --- a/app/database/crud/subscription_event.py +++ b/app/database/crud/subscription_event.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections.abc import Iterable -from datetime import datetime +from datetime import UTC, datetime from typing import Any from sqlalchemy import and_, func, select @@ -32,7 +32,7 @@ async def create_subscription_event( amount_kopeks=amount_kopeks, currency=currency, message=message, - occurred_at=occurred_at or datetime.utcnow(), + occurred_at=occurred_at or datetime.now(UTC), extra=extra or None, ) db.add(event) diff --git a/app/database/crud/ticket.py b/app/database/crud/ticket.py index 3d821e07..33714dff 100644 --- a/app/database/crud/ticket.py +++ b/app/database/crud/ticket.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import UTC, datetime import structlog from sqlalchemy import and_, desc, func, or_, select, update @@ -154,18 +154,18 @@ class TicketCRUD: tickets = result.scalars().all() if not tickets: return None - from datetime import datetime + from datetime import UTC, datetime # Если есть вечная блокировка в любом тикете — блок активен без срока for t in tickets: 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] if not future_until: return None 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 async def get_all_tickets( @@ -225,7 +225,7 @@ class TicketCRUD: return False ticket.status = status - ticket.updated_at = datetime.utcnow() + ticket.updated_at = datetime.now(UTC) if status == TicketStatus.CLOSED.value and closed_at: ticket.closed_at = closed_at @@ -259,14 +259,14 @@ class TicketCRUD: return False ticket.user_reply_block_permanent = bool(permanent) ticket.user_reply_block_until = until - ticket.updated_at = datetime.utcnow() + ticket.updated_at = datetime.now(UTC) await db.commit() return True @staticmethod 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 async def close_all_open_tickets( @@ -280,7 +280,7 @@ class TicketCRUD: if not ticket_ids: return [] - now = datetime.utcnow() + now = datetime.now(UTC) await db.execute( update(Ticket) .where(Ticket.id.in_(ticket_ids)) @@ -417,7 +417,7 @@ class TicketMessageCRUD: except Exception: pass - ticket.updated_at = datetime.utcnow() + ticket.updated_at = datetime.now(UTC) await db.commit() await db.refresh(message) diff --git a/app/database/crud/ticket_notification.py b/app/database/crud/ticket_notification.py index a76ba244..42200cea 100644 --- a/app/database/crud/ticket_notification.py +++ b/app/database/crud/ticket_notification.py @@ -1,6 +1,6 @@ """CRUD operations for TicketNotification.""" -from datetime import datetime +from datetime import UTC, datetime import structlog from sqlalchemy import desc, func, select, update @@ -40,7 +40,7 @@ class TicketNotificationCRUD: message=message, is_for_admin=is_for_admin, is_read=False, - created_at=datetime.utcnow(), + created_at=datetime.now(UTC), ) db.add(notification) await db.commit() @@ -130,7 +130,7 @@ class TicketNotificationCRUD: query = ( update(TicketNotification) .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) await db.commit() @@ -146,7 +146,7 @@ class TicketNotificationCRUD: TicketNotification.is_for_admin == 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) await db.commit() @@ -161,7 +161,7 @@ class TicketNotificationCRUD: TicketNotification.is_for_admin == True, 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) await db.commit() @@ -178,7 +178,7 @@ class TicketNotificationCRUD: TicketNotification.ticket_id == ticket_id, TicketNotification.is_read == False, ) - .values(is_read=True, read_at=datetime.utcnow()) + .values(is_read=True, read_at=datetime.now(UTC)) ) if is_admin: diff --git a/app/database/crud/transaction.py b/app/database/crud/transaction.py index a879342e..b40350f1 100644 --- a/app/database/crud/transaction.py +++ b/app/database/crud/transaction.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta import structlog 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, external_id=external_id, 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 {}), ) @@ -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: transaction.is_completed = True - transaction.completed_at = datetime.utcnow() + transaction.completed_at = datetime.now(UTC) await db.commit() 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 ) -> dict: 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: - end_date = datetime.utcnow() + end_date = datetime.now(UTC) # Доход считаем только по реальным платежам (исключаем колесо, промокоды, админские пополнения) 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 } - today = datetime.utcnow().date() + today = datetime.now(UTC).date() today_result = await db.execute( select(func.count(Transaction.id)).where( 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]: """Доход по дням - только реальные платежи.""" - start_date = datetime.utcnow() - timedelta(days=days) + start_date = datetime.now(UTC) - timedelta(days=days) result = await db.execute( select( @@ -363,7 +363,7 @@ async def find_tribute_transactions_by_payment_id( async def check_tribute_payment_duplicate( db: AsyncSession, payment_id: str, amount_kopeks: int, user_telegram_id: int ) -> Transaction | None: - cutoff_time = datetime.utcnow() - timedelta(hours=24) + cutoff_time = datetime.now(UTC) - timedelta(hours=24) 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) if existing: - timestamp = int(datetime.utcnow().timestamp()) + timestamp = int(datetime.now(UTC).timestamp()) external_id = f'donation_{payment_id}_{amount_kopeks}_{timestamp}' logger.info('Создан уникальный external_id для избежания дубликатов', external_id=external_id) diff --git a/app/database/crud/user.py b/app/database/crud/user.py index b3765fa7..792bfc32 100644 --- a/app/database/crud/user.py +++ b/app/database/crud/user.py @@ -195,7 +195,7 @@ async def create_unique_referral_code(db: AsyncSession) -> str: if not existing_user: return code - timestamp = str(int(datetime.utcnow().timestamp()))[-6:] + timestamp = str(int(datetime.now(UTC).timestamp()))[-6:] return f'ref{timestamp}' @@ -379,7 +379,7 @@ async def update_user(db: AsyncSession, user: User, **kwargs) -> User: if hasattr(user, field): setattr(user, field, value) - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) await db.commit() await db.refresh(user) @@ -399,7 +399,7 @@ async def add_user_balance( try: old_balance = user.balance_kopeks user.balance_kopeks += amount_kopeks - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) if create_transaction: 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_expires_at = None - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) if create_transaction: 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: - now = datetime.utcnow() + now = datetime.now(UTC) result = await db.execute( select(User).where( 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]: - now = datetime.utcnow() + now = datetime.now(UTC) base_query = ( 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]: - threshold_date = datetime.utcnow() - timedelta(days=months * 30) + threshold_date = datetime.now(UTC) - timedelta(days=months * 30) result = await db.execute( 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: user.status = UserStatus.DELETED.value - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) await db.commit() 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_users = active_result.scalar() - today = datetime.utcnow().date() + today = datetime.now(UTC).date() today_result = await db.execute( select(func.count(User.id)).where(and_(User.created_at >= today, User.status == UserStatus.ACTIVE.value)) ) 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( select(func.count(User.id)).where(and_(User.created_at >= week_ago, User.status == UserStatus.ACTIVE.value)) ) 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( 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: Список пользователей с активными подписками и remnawave_uuid """ - current_time = datetime.utcnow() + current_time = datetime.now(UTC) result = await db.execute( select(User) @@ -1177,7 +1177,7 @@ async def set_email_change_pending( user.email_change_new = new_email user.email_change_code = code user.email_change_expires = expires_at - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) await db.commit() 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: 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 user.email_change_new = 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 user.email = new_email 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_code = None user.email_change_expires = None - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) await db.commit() 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_code = None user.email_change_expires = None - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) await db.commit() 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 value: str | int = int(provider_id) if provider == 'vk' else provider_id 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) diff --git a/app/database/crud/user_message.py b/app/database/crud/user_message.py index c8f69d48..aa2c665f 100644 --- a/app/database/crud/user_message.py +++ b/app/database/crud/user_message.py @@ -1,5 +1,5 @@ import random -from datetime import datetime +from datetime import UTC, datetime import structlog from sqlalchemy import func, select @@ -111,7 +111,7 @@ async def update_user_message( if sort_order is not None: message.sort_order = sort_order - message.updated_at = datetime.utcnow() + message.updated_at = datetime.now(UTC) await db.commit() await db.refresh(message) @@ -127,7 +127,7 @@ async def toggle_user_message_status(db: AsyncSession, message_id: int) -> UserM return None message.is_active = not message.is_active - message.updated_at = datetime.utcnow() + message.updated_at = datetime.now(UTC) await db.commit() await db.refresh(message) diff --git a/app/database/crud/user_promo_group.py b/app/database/crud/user_promo_group.py index 1e1e0361..ed0b56d5 100644 --- a/app/database/crud/user_promo_group.py +++ b/app/database/crud/user_promo_group.py @@ -1,6 +1,6 @@ """CRUD операции для связи пользователей с промогруппами (Many-to-Many).""" -from datetime import datetime +from datetime import UTC, datetime import structlog 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: user.promo_group_id = new_primary_id - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) except Exception as error: logger.error('Ошибка синхронизации primary промогруппы пользователя', user_id=user_id, error=error) diff --git a/app/database/crud/web_api_token.py b/app/database/crud/web_api_token.py index d304d30e..d23ee7ff 100644 --- a/app/database/crud/web_api_token.py +++ b/app/database/crud/web_api_token.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Iterable -from datetime import datetime +from datetime import UTC, datetime from sqlalchemy import select, update from sqlalchemy.ext.asyncio import AsyncSession @@ -71,7 +71,7 @@ async def update_token( for key, value in kwargs.items(): if hasattr(token, key): setattr(token, key, value) - token.updated_at = datetime.utcnow() + token.updated_at = datetime.now(UTC) await db.flush() await db.refresh(token) return token @@ -86,7 +86,7 @@ async def set_tokens_active_status( await db.execute( update(WebApiToken) .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)) ) diff --git a/app/database/crud/webhook.py b/app/database/crud/webhook.py index 285b6382..e6d95ac0 100644 --- a/app/database/crud/webhook.py +++ b/app/database/crud/webhook.py @@ -1,6 +1,6 @@ from __future__ import annotations -from datetime import datetime +from datetime import UTC, datetime from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession @@ -94,7 +94,7 @@ async def update_webhook( if is_active is not None: webhook.is_active = is_active - webhook.updated_at = datetime.utcnow() + webhook.updated_at = datetime.now(UTC) await db.commit() await db.refresh(webhook) return webhook @@ -127,7 +127,7 @@ async def record_webhook_delivery( response_body=response_body, error_message=error_message, 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) await db.commit() @@ -145,7 +145,7 @@ async def update_webhook_stats( webhook.success_count += 1 else: webhook.failure_count += 1 - webhook.last_triggered_at = datetime.utcnow() + webhook.last_triggered_at = datetime.now(UTC) await db.commit() await db.refresh(webhook) return webhook diff --git a/app/database/crud/welcome_text.py b/app/database/crud/welcome_text.py index b48df45e..cf305dd6 100644 --- a/app/database/crud/welcome_text.py +++ b/app/database/crud/welcome_text.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import UTC, datetime import structlog 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: 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.refresh(welcome_text) @@ -182,7 +182,7 @@ async def update_welcome_text( if is_active is not None: welcome_text.is_active = is_active - welcome_text.updated_at = datetime.utcnow() + welcome_text.updated_at = datetime.now(UTC) await db.commit() await db.refresh(welcome_text) diff --git a/app/database/crud/wheel.py b/app/database/crud/wheel.py index a494a075..0c5c909d 100644 --- a/app/database/crud/wheel.py +++ b/app/database/crud/wheel.py @@ -2,7 +2,7 @@ CRUD операции для колеса удачи (Fortune Wheel). """ -from datetime import datetime +from datetime import UTC, datetime from typing import Any import structlog @@ -65,7 +65,7 @@ async def update_wheel_config(db: AsyncSession, **kwargs) -> WheelConfig: if hasattr(config, key) and value is not None: setattr(config, key, value) - config.updated_at = datetime.utcnow() + config.updated_at = datetime.now(UTC) await db.commit() await db.refresh(config) 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: setattr(prize, key, value) - prize.updated_at = datetime.utcnow() + prize.updated_at = datetime.now(UTC) await db.commit() await db.refresh(prize) logger.info('🎁 Обновлен приз колеса ID', prize_id=prize_id, kwargs=kwargs) @@ -204,7 +204,7 @@ async def create_wheel_spin( prize_value_kopeks=prize_value_kopeks, generated_promocode_id=generated_promocode_id, 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) 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() if spin: spin.is_applied = True - spin.applied_at = datetime.utcnow() + spin.applied_at = datetime.now(UTC) await db.commit() await db.refresh(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: """Получить количество спинов пользователя за сегодня.""" - 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( select(func.count(WheelSpin.id)).where( diff --git a/app/database/crud/yookassa.py b/app/database/crud/yookassa.py index 05153833..0841e56c 100644 --- a/app/database/crud/yookassa.py +++ b/app/database/crud/yookassa.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import UTC, datetime import structlog from sqlalchemy import and_, select, update @@ -88,7 +88,7 @@ async def update_yookassa_payment_status( captured_at: datetime | None = None, payment_method_type: str | None = 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: update_data['captured_at'] = captured_at @@ -125,7 +125,7 @@ async def link_yookassa_payment_to_transaction( await db.execute( update(YooKassaPayment) .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() diff --git a/app/database/models.py b/app/database/models.py index 599ed9cf..67dfa144 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -1,4 +1,4 @@ -from datetime import datetime, time, timedelta +from datetime import UTC, datetime, time, timedelta from enum import Enum from sqlalchemy import ( @@ -176,10 +176,10 @@ class YooKassaPayment(Base): payment_method_type = Column(String(50), nullable=True) refundable = Column(Boolean, default=False) test_mode = Column(Boolean, default=False) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) - yookassa_created_at = Column(DateTime, nullable=True) - captured_at = Column(DateTime, nullable=True) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) + yookassa_created_at = Column(DateTime(timezone=True), nullable=True) + captured_at = Column(DateTime(timezone=True), nullable=True) user = relationship('User', backref='yookassa_payments') transaction = relationship('Transaction', backref='yookassa_payment') @@ -225,11 +225,11 @@ class CryptoBotPayment(Base): mini_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) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) user = relationship('User', backref='cryptobot_payments') transaction = relationship('Transaction', backref='cryptobot_payment') @@ -277,12 +277,12 @@ class HeleketPayment(Base): payment_url = Column(Text, nullable=True) metadata_json = Column(JSON, nullable=True) - paid_at = Column(DateTime, nullable=True) - expires_at = Column(DateTime, nullable=True) + paid_at = Column(DateTime(timezone=True), nullable=True) + expires_at = Column(DateTime(timezone=True), nullable=True) transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) user = relationship('User', backref='heleket_payments') transaction = relationship('Transaction', backref='heleket_payment') @@ -330,7 +330,7 @@ class MulenPayPayment(Base): status = Column(String(50), nullable=False, default='created') 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) metadata_json = Column(JSON, nullable=True) @@ -338,8 +338,8 @@ class MulenPayPayment(Base): transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) user = relationship('User', backref='mulenpay_payments') transaction = relationship('Transaction', backref='mulenpay_payment') @@ -368,9 +368,9 @@ class Pal24Payment(Base): status = Column(String(50), nullable=False, default='NEW') is_active = Column(Boolean, default=True) 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_checked_at = Column(DateTime, nullable=True) + last_status_checked_at = Column(DateTime(timezone=True), nullable=True) link_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) 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) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) user = relationship('User', backref='pal24_payments') transaction = relationship('Transaction', backref='pal24_payment') @@ -424,7 +424,7 @@ class WataPayment(Base): status = Column(String(50), nullable=False, default='Opened') 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) terminal_public_id = Column(String(64), nullable=True) @@ -434,12 +434,12 @@ class WataPayment(Base): metadata_json = 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) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) user = relationship('User', backref='wata_payments') transaction = relationship('Transaction', backref='wata_payment') @@ -467,7 +467,7 @@ class PlategaPayment(Base): payment_method_code = Column(Integer, nullable=False) status = Column(String(50), nullable=False, default='PENDING') 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) return_url = Column(Text, nullable=True) @@ -476,12 +476,12 @@ class PlategaPayment(Base): metadata_json = 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) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) user = relationship('User', backref='platega_payments') 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 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) @@ -537,8 +537,8 @@ class CloudPaymentsPayment(Base): # Связь с транзакцией в нашей системе transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) user = relationship('User', backref='cloudpayments_payments') transaction = relationship('Transaction', backref='cloudpayments_payment') @@ -591,10 +591,10 @@ class FreekassaPayment(Base): callback_payload = Column(JSON, nullable=True) # Временные метки - paid_at = Column(DateTime, nullable=True) - expires_at = Column(DateTime, nullable=True) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + paid_at = Column(DateTime(timezone=True), nullable=True) + expires_at = Column(DateTime(timezone=True), nullable=True) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) # Связь с транзакцией transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True) @@ -653,10 +653,10 @@ class KassaAiPayment(Base): callback_payload = Column(JSON, nullable=True) # Временные метки - paid_at = Column(DateTime, nullable=True) - expires_at = Column(DateTime, nullable=True) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + paid_at = Column(DateTime(timezone=True), nullable=True) + expires_at = Column(DateTime(timezone=True), nullable=True) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) # Связь с транзакцией 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) apply_discounts_to_addons = Column(Boolean, nullable=False, default=True) is_default = Column(Boolean, nullable=False, default=False) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) users = relationship('User', back_populates='promo_group') 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) 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') user = relationship('User', back_populates='user_promo_groups') @@ -851,8 +851,8 @@ class Tariff(Base): # Режим сброса трафика: DAY, WEEK, MONTH, NO_RESET (по умолчанию берётся из конфига) traffic_reset_mode = Column(String(20), nullable=True, default=None) # None = использовать глобальную настройку - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) # M2M связь с промогруппами (какие промогруппы имеют доступ к тарифу) allowed_promo_groups = relationship( @@ -976,25 +976,25 @@ class User(Base): has_had_paid_subscription = Column(Boolean, default=False, nullable=False) referred_by_id = Column(Integer, ForeignKey('users.id'), nullable=True) referral_code = Column(String(20), unique=True, nullable=True) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) - last_activity = Column(DateTime, default=func.now()) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) + last_activity = Column(DateTime(timezone=True), default=func.now()) remnawave_uuid = Column(String(255), nullable=True, unique=True) # Cabinet authentication fields email = Column(String(255), unique=True, nullable=True, index=True) 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) 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_expires = Column(DateTime, nullable=True) - cabinet_last_login = Column(DateTime, nullable=True) + password_reset_expires = Column(DateTime(timezone=True), nullable=True) + cabinet_last_login = Column(DateTime(timezone=True), nullable=True) # Email change fields 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_expires = Column(DateTime, nullable=True) # Code expiration + email_change_expires = Column(DateTime(timezone=True), nullable=True) # Code expiration # OAuth provider IDs google_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) promo_offer_discount_percent = Column(Integer, nullable=False, default=0) promo_offer_discount_source = Column(String(100), nullable=True) - promo_offer_discount_expires_at = Column(DateTime, nullable=True) - last_remnawave_sync = Column(DateTime, nullable=True) + promo_offer_discount_expires_at = Column(DateTime(timezone=True), nullable=True) + last_remnawave_sync = Column(DateTime(timezone=True), nullable=True) trojan_password = Column(String(255), nullable=True) vless_uuid = 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) is_trial = Column(Boolean, default=True) - start_date = Column(DateTime, default=func.now()) - end_date = Column(DateTime, nullable=False) + start_date = Column(DateTime(timezone=True), default=func.now()) + end_date = Column(DateTime(timezone=True), nullable=False) traffic_limit_gb = Column(Integer, default=0) traffic_used_gb = Column(Float, default=0.0) purchased_traffic_gb = Column(Integer, default=0) # Докупленный трафик traffic_reset_at = Column( - DateTime, nullable=True + DateTime(timezone=True), nullable=True ) # Дата сброса докупленного трафика (30 дней после первой докупки) subscription_url = Column(String, nullable=True) @@ -1134,10 +1134,10 @@ class Subscription(Base): autopay_enabled = Column(Boolean, default=False) autopay_days_before = Column(Integer, default=3) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), default=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) @@ -1148,7 +1148,7 @@ class Subscription(Base): is_daily_paused = Column( 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') tariff = relationship('Tariff', back_populates='subscriptions') @@ -1162,7 +1162,7 @@ class Subscription(Base): @property def is_active(self) -> bool: - current_time = datetime.utcnow() + current_time = datetime.now(UTC) return ( self.status == SubscriptionStatus.ACTIVE.value and self.end_date is not None @@ -1172,11 +1172,11 @@ class Subscription(Base): @property 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 def should_be_expired(self) -> bool: - current_time = datetime.utcnow() + current_time = datetime.now(UTC) return ( self.status == SubscriptionStatus.ACTIVE.value and self.end_date is not None @@ -1185,7 +1185,7 @@ class Subscription(Base): @property def actual_status(self) -> str: - current_time = datetime.utcnow() + current_time = datetime.now(UTC) if self.status == SubscriptionStatus.EXPIRED.value: return 'expired' @@ -1208,7 +1208,6 @@ class Subscription(Base): @property def status_display(self) -> str: actual_status = self.actual_status - datetime.utcnow() if actual_status == 'expired': return '🔴 Истекла' @@ -1244,7 +1243,7 @@ class Subscription(Base): def days_left(self) -> int: if self.end_date is None: return 0 - current_time = datetime.utcnow() + current_time = datetime.now(UTC) if self.end_date <= current_time: return 0 delta = self.end_date - current_time @@ -1252,7 +1251,7 @@ class Subscription(Base): @property def time_left_display(self) -> str: - current_time = datetime.utcnow() + current_time = datetime.now(UTC) if self.end_date <= current_time: return 'истёк' @@ -1275,10 +1274,10 @@ class Subscription(Base): return min((used / self.traffic_limit_gb) * 100, 100.0) 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) else: - self.end_date = datetime.utcnow() + timedelta(days=days) + self.end_date = datetime.now(UTC) + timedelta(days=days) if self.status == SubscriptionStatus.EXPIRED.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) 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') @property def is_expired(self) -> bool: """Проверяет, истекла ли докупка.""" - return datetime.utcnow() >= self.expires_at + return datetime.now(UTC) >= self.expires_at class Transaction(Base): @@ -1352,10 +1351,10 @@ class Transaction(Base): # NaloGO чек 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()) - completed_at = Column(DateTime, nullable=True) + created_at = Column(DateTime(timezone=True), default=func.now()) + completed_at = Column(DateTime(timezone=True), nullable=True) user = relationship('User', back_populates='transactions') @@ -1370,7 +1369,7 @@ class SubscriptionConversion(Base): id = Column(Integer, primary_key=True, index=True) 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) @@ -1380,7 +1379,7 @@ class SubscriptionConversion(Base): 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') @@ -1406,8 +1405,8 @@ class PromoCode(Base): max_uses = Column(Integer, default=1) current_uses = Column(Integer, default=0) - valid_from = Column(DateTime, default=func.now()) - valid_until = Column(DateTime, nullable=True) + valid_from = Column(DateTime(timezone=True), default=func.now()) + valid_until = Column(DateTime(timezone=True), nullable=True) is_active = Column(Boolean, default=True) first_purchase_only = Column(Boolean, default=False) # Только для первой покупки @@ -1415,15 +1414,15 @@ class PromoCode(Base): 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) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) uses = relationship('PromoCodeUse', back_populates='promocode') promo_group = relationship('PromoGroup') @property def is_valid(self) -> bool: - now = datetime.utcnow() + now = datetime.now(UTC) return ( self.is_active and self.current_uses < self.max_uses @@ -1443,7 +1442,7 @@ class PromoCodeUse(Base): promocode_id = Column(Integer, ForeignKey('promocodes.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') user = relationship('User') @@ -1461,7 +1460,7 @@ class ReferralEarning(Base): 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') referral = relationship('User', foreign_keys=[referral_id]) @@ -1502,11 +1501,11 @@ class WithdrawalRequest(Base): # Обработка админом 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) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) user = relationship('User', foreign_keys=[user_id], backref='withdrawal_requests') admin = relationship('User', foreign_keys=[processed_by]) @@ -1524,18 +1523,18 @@ class ReferralContest(Base): description = Column(Text, nullable=True) prize_text = Column(Text, nullable=True) contest_type = Column(String(50), nullable=False, default='referral_paid') - start_at = Column(DateTime, nullable=False) - end_at = Column(DateTime, nullable=False) + start_at = Column(DateTime(timezone=True), 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_times = Column(String(255), nullable=True) # CSV HH:MM timezone = Column(String(64), nullable=False, default='UTC') is_active = Column(Boolean, nullable=False, default=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) created_by = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) creator = relationship('User', backref='created_referral_contests') events = relationship( @@ -1565,7 +1564,7 @@ class ReferralContestEvent(Base): referral_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), nullable=False) event_type = Column(String(50), nullable=False) 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') referrer = relationship('User', foreign_keys=[referrer_id]) @@ -1585,7 +1584,7 @@ class ReferralContestVirtualParticipant(Base): display_name = Column(String(255), nullable=False) referral_count = 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') @@ -1611,8 +1610,8 @@ class ContestTemplate(Base): cooldown_hours = Column(Integer, nullable=False, default=24) payload = Column(JSON, nullable=True) is_enabled = Column(Boolean, nullable=False, default=True) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) rounds = relationship('ContestRound', back_populates='template') @@ -1626,8 +1625,8 @@ class ContestRound(Base): id = Column(Integer, primary_key=True, index=True) template_id = Column(Integer, ForeignKey('contest_templates.id', ondelete='CASCADE'), nullable=False) - starts_at = Column(DateTime, nullable=False) - ends_at = Column(DateTime, nullable=False) + starts_at = Column(DateTime(timezone=True), nullable=False) + ends_at = Column(DateTime(timezone=True), nullable=False) status = Column(String(20), nullable=False, default='active') # active, finished payload = Column(JSON, nullable=True) winners_count = Column(Integer, nullable=False, default=0) @@ -1635,8 +1634,8 @@ class ContestRound(Base): attempts_per_user = Column(Integer, nullable=False, default=1) message_id = Column(BigInteger, nullable=True) chat_id = Column(BigInteger, nullable=True) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) template = relationship('ContestTemplate', back_populates='rounds') 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) answer = Column(Text, nullable=True) 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') user = relationship('User') @@ -1674,8 +1673,8 @@ class Squad(Base): description = Column(Text, nullable=True) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) @property def price_rubles(self) -> float: @@ -1696,8 +1695,8 @@ class ServiceRule(Base): language = Column(String(5), default='ru') - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) class PrivacyPolicy(Base): @@ -1707,8 +1706,8 @@ class PrivacyPolicy(Base): language = Column(String(10), nullable=False, unique=True) content = Column(Text, nullable=False) is_enabled = Column(Boolean, default=True, nullable=False) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) class PublicOffer(Base): @@ -1718,8 +1717,8 @@ class PublicOffer(Base): language = Column(String(10), nullable=False, unique=True) content = Column(Text, nullable=False) is_enabled = Column(Boolean, default=True, nullable=False) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) class FaqSetting(Base): @@ -1728,8 +1727,8 @@ class FaqSetting(Base): id = Column(Integer, primary_key=True, index=True) language = Column(String(10), nullable=False, unique=True) is_enabled = Column(Boolean, default=True, nullable=False) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) class FaqPage(Base): @@ -1741,8 +1740,8 @@ class FaqPage(Base): content = Column(Text, nullable=False) display_order = Column(Integer, default=0, nullable=False) is_active = Column(Boolean, default=True, nullable=False) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) class SystemSetting(Base): @@ -1753,8 +1752,8 @@ class SystemSetting(Base): value = Column(Text, nullable=True) description = Column(Text, nullable=True) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) class MonitoringLog(Base): @@ -1769,7 +1768,7 @@ class MonitoringLog(Base): 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): @@ -1780,7 +1779,7 @@ class SentNotification(Base): subscription_id = Column(Integer, ForeignKey('subscriptions.id', ondelete='CASCADE'), nullable=False) notification_type = Column(String(50), nullable=False) 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') subscription = relationship('Subscription', backref=backref('sent_notifications', passive_deletes=True)) @@ -1797,9 +1796,9 @@ class SubscriptionEvent(Base): amount_kopeks = Column(Integer, nullable=True) currency = Column(String(16), 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) - created_at = Column(DateTime, default=func.now()) + created_at = Column(DateTime(timezone=True), default=func.now()) user = relationship('User', backref='subscription_events') subscription = relationship('Subscription', backref='subscription_events') @@ -1816,13 +1815,13 @@ class DiscountOffer(Base): notification_type = Column(String(50), nullable=False) discount_percent = Column(Integer, nullable=False, default=0) bonus_amount_kopeks = Column(Integer, nullable=False, default=0) - expires_at = Column(DateTime, nullable=False) - claimed_at = Column(DateTime, nullable=True) + expires_at = Column(DateTime(timezone=True), nullable=False) + claimed_at = Column(DateTime(timezone=True), nullable=True) is_active = Column(Boolean, default=True, nullable=False) effect_type = Column(String(50), nullable=False, default='percent_discount') extra_data = Column(JSON, nullable=True) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) user = relationship('User', 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) is_active = Column(Boolean, default=True, nullable=False) created_by = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) creator = relationship('User') @@ -1859,9 +1858,9 @@ class SubscriptionTemporaryAccess(Base): subscription_id = Column(Integer, ForeignKey('subscriptions.id', ondelete='CASCADE'), nullable=False) offer_id = Column(Integer, ForeignKey('discount_offers.id', ondelete='CASCADE'), nullable=False) squad_uuid = Column(String(255), nullable=False) - expires_at = Column(DateTime, nullable=False) - created_at = Column(DateTime, default=func.now()) - deactivated_at = Column(DateTime, nullable=True) + expires_at = Column(DateTime(timezone=True), nullable=False) + created_at = Column(DateTime(timezone=True), default=func.now()) + deactivated_at = Column(DateTime(timezone=True), nullable=True) is_active = Column(Boolean, default=True, nullable=False) was_already_connected = Column(Boolean, default=False, nullable=False) @@ -1880,7 +1879,7 @@ class PromoOfferLog(Base): percent = Column(Integer, nullable=True) effect_type = Column(String(50), 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') offer = relationship('DiscountOffer', back_populates='logs') @@ -1922,8 +1921,8 @@ class Poll(Base): reward_enabled = Column(Boolean, nullable=False, default=False) reward_amount_kopeks = Column(Integer, nullable=False, default=0) created_by = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True) - created_at = Column(DateTime, default=func.now(), nullable=False) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now(), nullable=False) + created_at = Column(DateTime(timezone=True), default=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]) questions = relationship( @@ -1975,9 +1974,9 @@ class PollResponse(Base): id = Column(Integer, primary_key=True, 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) - sent_at = Column(DateTime, default=func.now(), nullable=False) - started_at = Column(DateTime, nullable=True) - completed_at = Column(DateTime, nullable=True) + sent_at = Column(DateTime(timezone=True), default=func.now(), nullable=False) + started_at = Column(DateTime(timezone=True), nullable=True) + completed_at = Column(DateTime(timezone=True), nullable=True) reward_given = Column(Boolean, nullable=False, default=False) 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) 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) - 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') question = relationship('PollQuestion', back_populates='answers') @@ -2033,8 +2032,8 @@ class ServerSquad(Base): max_users = Column(Integer, nullable=True) current_users = Column(Integer, default=0) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) allowed_promo_groups = relationship( 'PromoGroup', @@ -2069,7 +2068,7 @@ class SubscriptionServer(Base): subscription_id = Column(Integer, ForeignKey('subscriptions.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) @@ -2088,7 +2087,7 @@ class SupportAuditLog(Base): 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) 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]) ticket = relationship('Ticket', foreign_keys=[ticket_id]) @@ -2101,8 +2100,8 @@ class UserMessage(Base): is_active = Column(Boolean, default=True) sort_order = Column(Integer, default=0) created_by = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) creator = relationship('User', backref='created_messages') def __repr__(self): @@ -2117,8 +2116,8 @@ class WelcomeText(Base): is_active = Column(Boolean, default=True) is_enabled = Column(Boolean, default=True) created_by = Column(Integer, ForeignKey('users.id'), nullable=True) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) 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) is_active = Column(Boolean, default=True) created_by = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) creator = relationship('User', backref='pinned_messages') @@ -2162,8 +2161,8 @@ class AdvertisingCampaign(Base): is_active = Column(Boolean, default=True) created_by = Column(Integer, ForeignKey('users.id'), nullable=True) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) registrations = relationship('AdvertisingCampaignRegistration', back_populates='campaign') 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_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') user = relationship('User') @@ -2232,13 +2231,13 @@ class Ticket(Base): 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_until = Column(DateTime, nullable=True) + user_reply_block_until = Column(DateTime(timezone=True), nullable=True) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) - closed_at = Column(DateTime, nullable=True) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) + closed_at = Column(DateTime(timezone=True), nullable=True) # 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') @@ -2266,9 +2265,9 @@ class Ticket(Base): return True if self.user_reply_block_until: 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: return True return False @@ -2308,7 +2307,7 @@ class TicketMessage(Base): media_file_id = Column(String(255), 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') @@ -2334,10 +2333,10 @@ class WebApiToken(Base): token_hash = Column(String(128), nullable=False, unique=True, index=True) token_prefix = Column(String(32), nullable=False, index=True) description = Column(Text, nullable=True) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) - expires_at = Column(DateTime, nullable=True) - last_used_at = Column(DateTime, nullable=True) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) + expires_at = Column(DateTime(timezone=True), nullable=True) + last_used_at = Column(DateTime(timezone=True), nullable=True) last_used_ip = Column(String(64), nullable=True) is_active = Column(Boolean, default=True, nullable=False) created_by = Column(String(255), nullable=True) @@ -2357,8 +2356,8 @@ class MainMenuButton(Base): visibility = Column(String(20), nullable=False, default=MainMenuButtonVisibility.ALL.value) is_active = Column(Boolean, nullable=False, default=True) display_order = Column(Integer, nullable=False, default=0) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), default=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'),) @@ -2393,7 +2392,7 @@ class MenuLayoutHistory(Base): action = Column(String(50), nullable=False) # update, reset, import changes_summary = Column(Text, 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'),) @@ -2410,7 +2409,7 @@ class ButtonClickLog(Base): button_id = Column(String(100), nullable=False, index=True) # ID кнопки user_id = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True, index=True) 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 @@ -2444,9 +2443,9 @@ class Webhook(Base): event_type = Column(String(50), nullable=False) # user.created, payment.completed, ticket.created, etc. is_active = Column(Boolean, default=True, nullable=False) description = Column(Text, nullable=True) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) - last_triggered_at = Column(DateTime, nullable=True) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) + last_triggered_at = Column(DateTime(timezone=True), nullable=True) failure_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 error_message = Column(Text, nullable=True) attempt_number = Column(Integer, default=1, nullable=False) - created_at = Column(DateTime, default=func.now()) - delivered_at = Column(DateTime, nullable=True) - next_retry_at = Column(DateTime, nullable=True) + created_at = Column(DateTime(timezone=True), default=func.now()) + delivered_at = Column(DateTime(timezone=True), nullable=True) + next_retry_at = Column(DateTime(timezone=True), nullable=True) webhook = relationship('Webhook', back_populates='deliveries') @@ -2495,15 +2494,15 @@ class CabinetRefreshToken(Base): user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), nullable=False) token_hash = Column(String(255), unique=True, nullable=False, index=True) device_info = Column(String(500), nullable=True) - expires_at = Column(DateTime, nullable=False) - created_at = Column(DateTime, default=func.now()) - revoked_at = Column(DateTime, nullable=True) + expires_at = Column(DateTime(timezone=True), nullable=False) + created_at = Column(DateTime(timezone=True), default=func.now()) + revoked_at = Column(DateTime(timezone=True), nullable=True) user = relationship('User', backref='cabinet_tokens') @property def is_expired(self) -> bool: - return datetime.utcnow() > self.expires_at + return datetime.now(UTC) > self.expires_at @property def is_revoked(self) -> bool: @@ -2549,8 +2548,8 @@ class WheelConfig(Base): promo_prefix = Column(String(20), default='WHEEL', nullable=False) promo_validity_days = Column(Integer, default=7, nullable=False) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) 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_traffic_gb = Column(Integer, default=0) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) config = relationship('WheelConfig', back_populates='prizes') spins = relationship('WheelSpin', back_populates='prize') @@ -2624,9 +2623,9 @@ class WheelSpin(Base): # Флаг успешного начисления 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') prize = relationship('WheelPrize', back_populates='spins') @@ -2671,8 +2670,8 @@ class TicketNotification(Base): # Прочитано ли уведомление is_read = Column(Boolean, default=False, nullable=False) - created_at = Column(DateTime, default=func.now()) - read_at = Column(DateTime, nullable=True) + created_at = Column(DateTime(timezone=True), default=func.now()) + read_at = Column(DateTime(timezone=True), nullable=True) ticket = relationship('Ticket', backref='notifications') user = relationship('User', backref='ticket_notifications') @@ -2729,8 +2728,8 @@ class PaymentMethodConfig(Base): lazy='selectin', ) - created_at = Column(DateTime, default=func.now()) - updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + created_at = Column(DateTime(timezone=True), default=func.now()) + updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now()) def __repr__(self) -> str: return f"" diff --git a/app/database/universal_migration.py b/app/database/universal_migration.py index 85ed3da7..a28a1a9c 100644 --- a/app/database/universal_migration.py +++ b/app/database/universal_migration.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import UTC, datetime import structlog from sqlalchemy import select, text @@ -4720,7 +4720,7 @@ async def ensure_default_web_api_token() -> bool: updated = True if updated: - existing.updated_at = datetime.utcnow() + existing.updated_at = datetime.now(UTC) await session.commit() return True @@ -6454,6 +6454,63 @@ async def migrate_cloudpayments_transaction_id_to_bigint() -> bool: 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(): logger.info('=== НАЧАЛО УНИВЕРСАЛЬНОЙ МИГРАЦИИ ===') @@ -7130,6 +7187,13 @@ async def run_universal_migration(): else: 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: total_subs = await conn.execute(text('SELECT COUNT(*) FROM subscriptions')) unique_users = await conn.execute(text('SELECT COUNT(DISTINCT user_id) FROM subscriptions')) diff --git a/app/external/wata_webhook.py b/app/external/wata_webhook.py index dbcfbfe5..c0709248 100644 --- a/app/external/wata_webhook.py +++ b/app/external/wata_webhook.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio import base64 import json -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from typing import Any import aiohttp @@ -33,12 +33,12 @@ class WataPublicKeyProvider: async def get_public_key(self) -> str | None: """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: return self._cached_key async with self._lock: - now = datetime.utcnow() + now = datetime.now(UTC) if self._cached_key and self._expires_at and now < self._expires_at: return self._cached_key @@ -46,7 +46,7 @@ class WataPublicKeyProvider: if key: self._cached_key = key 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: self._expires_at = None logger.debug('Получен и закеширован публичный ключ WATA') diff --git a/app/handlers/admin/bot_configuration.py b/app/handlers/admin/bot_configuration.py index 454cc6ae..c24c2024 100644 --- a/app/handlers/admin/bot_configuration.py +++ b/app/handlers/admin/bot_configuration.py @@ -3,7 +3,7 @@ import io import math import time from collections.abc import Iterable -from datetime import datetime +from datetime import UTC, datetime from aiogram import Dispatcher, F, types from aiogram.filters import BaseFilter, StateFilter @@ -742,7 +742,7 @@ async def export_settings( keys = sorted(set(keys)) lines = [ '# 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: @@ -753,7 +753,7 @@ async def export_settings( lines.append(f'{setting_key}={raw_value}') 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) await callback.message.answer_document( diff --git a/app/handlers/admin/contests.py b/app/handlers/admin/contests.py index 0136fab6..3e379606 100644 --- a/app/handlers/admin/contests.py +++ b/app/handlers/admin/contests.py @@ -399,7 +399,7 @@ async def delete_contest( await callback.answer(texts.t('ADMIN_CONTEST_NOT_FOUND', 'Конкурс не найден.'), show_alert=True) return - now_utc = datetime.utcnow() + now_utc = datetime.now(UTC) if contest.is_active or contest.end_at > now_utc: await callback.answer( 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', 'Не удалось распознать дату.')) return - start_at = datetime.fromisoformat(start_at_raw).astimezone(UTC).replace(tzinfo=None) - end_at = datetime.fromisoformat(end_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) contest_type = data.get('contest_type') or 'referral_paid' diff --git a/app/handlers/admin/daily_contests.py b/app/handlers/admin/daily_contests.py index 69032479..5e16d4d6 100644 --- a/app/handlers/admin/daily_contests.py +++ b/app/handlers/admin/daily_contests.py @@ -1,5 +1,5 @@ import json -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta import structlog from aiogram import Dispatcher, F, types @@ -171,7 +171,7 @@ async def start_round_now( await db.refresh(tpl) 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) await create_round( db, @@ -182,8 +182,8 @@ async def start_round_now( ) await contest_rotation_service._announce_round_start( # type: ignore[attr-defined] tpl, - now.replace(tzinfo=None), - ends.replace(tzinfo=None), + now, + ends, ) await callback.answer(texts.t('ADMIN_ROUND_STARTED', 'Раунд запущен'), show_alert=True) 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] - now = datetime.utcnow() + now = datetime.now(UTC) ends = now + timedelta(hours=tpl.cooldown_hours) await create_round( db, @@ -227,8 +227,8 @@ async def manual_start_round( # Анонсируем всем пользователям (как тест) await contest_rotation_service._announce_round_start( # type: ignore[attr-defined] tpl, - now.replace(tzinfo=None), - ends.replace(tzinfo=None), + now, + ends, ) await callback.answer(texts.t('ADMIN_ROUND_STARTED', 'Тестовый раунд запущен'), show_alert=True) await show_daily_contest(callback, db_user, db) @@ -431,7 +431,7 @@ async def start_all_contests( continue # уже запущен 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) await create_round( db, @@ -442,8 +442,8 @@ async def start_all_contests( ) await contest_rotation_service._announce_round_start( # type: ignore[attr-defined] tpl, - now.replace(tzinfo=None), - ends.replace(tzinfo=None), + now, + ends, ) started_count += 1 diff --git a/app/handlers/admin/messages.py b/app/handlers/admin/messages.py index cd623ebc..d4db31b2 100644 --- a/app/handlers/admin/messages.py +++ b/app/handlers/admin/messages.py @@ -1,6 +1,6 @@ import asyncio import html -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta import structlog from aiogram import Dispatcher, F, types @@ -148,7 +148,7 @@ async def _persist_broadcast_result( failed_count: Количество неудачных отправок status: Финальный статус рассылки ('completed', 'partial', 'failed') """ - completed_at = datetime.utcnow() + completed_at = datetime.now(UTC) max_retries = 3 retry_delay = 1.0 @@ -309,7 +309,7 @@ async def toggle_pinned_message_position( return 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 show_pinned_message_menu(callback, db_user, db, state) @@ -329,7 +329,7 @@ async def toggle_pinned_message_start_mode( return 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 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: """Быстрый подсчёт пользователей через SQL COUNT вместо загрузки всех в память.""" - from datetime import datetime, timedelta + from datetime import UTC, datetime, timedelta 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': # Истекающие в ближайшие 3 дня - now = datetime.utcnow() + now = datetime.now(UTC) expiry_threshold = now + timedelta(days=3) query = ( 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': # Истекающие в ближайшие 7 дней - now = datetime.utcnow() + now = datetime.now(UTC) expiry_threshold = now + timedelta(days=7) query = ( 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': # Истекшие подписки - now = datetime.utcnow() + now = datetime.now(UTC) expired_statuses = [SubscriptionStatus.EXPIRED.value, SubscriptionStatus.DISABLED.value] query = ( 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': # То же что и expired - now = datetime.utcnow() + now = datetime.now(UTC) expired_statuses = [SubscriptionStatus.EXPIRED.value, SubscriptionStatus.DISABLED.value] query = ( 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 вместо загрузки всех пользователей if target.startswith('custom_'): - now = datetime.utcnow() + now = datetime.now(UTC) today = now.replace(hour=0, minute=0, second=0, microsecond=0) 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] if target == 'expired': - now = datetime.utcnow() + now = datetime.now(UTC) expired_statuses = { SubscriptionStatus.EXPIRED.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] if target == 'expired_subscribers': - now = datetime.utcnow() + now = datetime.now(UTC) expired_statuses = { SubscriptionStatus.EXPIRED.value, SubscriptionStatus.DISABLED.value, @@ -1782,7 +1782,7 @@ async def get_target_users(db: AsyncSession, target: str) -> list: ] if target == 'trial_ending': - now = datetime.utcnow() + now = datetime.now(UTC) in_3_days = now + timedelta(days=3) return [ user @@ -1794,7 +1794,7 @@ async def get_target_users(db: AsyncSession, target: str) -> list: ] if target == 'trial_expired': - now = datetime.utcnow() + now = datetime.now(UTC) return [ user for user in users @@ -1804,7 +1804,7 @@ async def get_target_users(db: AsyncSession, target: str) -> list: if target == 'autopay_failed': from app.database.models import SubscriptionEvent - week_ago = datetime.utcnow() - timedelta(days=7) + week_ago = datetime.now(UTC) - timedelta(days=7) stmt = ( select(SubscriptionEvent.user_id) .where( @@ -1826,15 +1826,15 @@ async def get_target_users(db: AsyncSession, target: str) -> list: ] 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] 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] 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] # Фильтр по тарифу @@ -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: - now = datetime.utcnow() + now = datetime.now(UTC) today = now.replace(hour=0, minute=0, second=0, microsecond=0) week_ago = now - timedelta(days=7) 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: - now = datetime.utcnow() + now = datetime.now(UTC) today = now.replace(hour=0, minute=0, second=0, microsecond=0) week_ago = now - timedelta(days=7) month_ago = now - timedelta(days=30) diff --git a/app/handlers/admin/monitoring.py b/app/handlers/admin/monitoring.py index 6cd23fdd..5dbaa5ac 100644 --- a/app/handlers/admin/monitoring.py +++ b/app/handlers/admin/monitoring.py @@ -139,7 +139,7 @@ def _build_notification_settings_view(language: str): def _build_notification_preview_message(language: str, notification_type: str): texts = get_texts(language) - now = datetime.now() + now = datetime.now(UTC) price_30_days = settings.format_price(settings.PRICE_30_DAYS) from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup @@ -694,7 +694,7 @@ async def force_check_callback(callback: CallbackQuery): • Истекающих подписок: {results['expiring']} • Готовых к автооплате: {results['autopay_ready']} -🕐 Время проверки: {datetime.now().strftime('%H:%M:%S')} +🕐 Время проверки: {datetime.now(UTC).strftime('%H:%M:%S')} Нажмите "Назад" для возврата в меню мониторинга. """ @@ -748,7 +748,7 @@ async def traffic_check_callback(callback: CallbackQuery): • Порог дельты: {threshold_gb} ГБ • Возраст snapshot: {snapshot_age:.1f} мин -🕐 Время проверки: {datetime.now().strftime('%H:%M:%S')} +🕐 Время проверки: {datetime.now(UTC).strftime('%H:%M:%S')} """ if violations: @@ -862,7 +862,7 @@ async def test_notifications_callback(callback: CallbackQuery): 📊 Статус системы: • Мониторинг: {'🟢 Работает' if monitoring_service.is_running 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) - 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 = [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) 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 = [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']) @@ -1235,7 +1235,7 @@ async def receipts_missing_callback(callback: CallbackQuery): async def receipts_link_old_callback(callback: CallbackQuery): """Привязать старые чеки из NaloGO к транзакциям по сумме и дате.""" try: - from datetime import date, timedelta + from datetime import UTC, date, timedelta from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from sqlalchemy import and_, select @@ -1245,7 +1245,7 @@ async def receipts_link_old_callback(callback: CallbackQuery): 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: # Получаем старые транзакции без чеков @@ -1311,9 +1311,12 @@ async def receipts_link_old_callback(callback: CallbackQuery): try: 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: - t.receipt_created_at = datetime.utcnow() + t.receipt_created_at = datetime.now(UTC) linked += 1 if linked > 0: diff --git a/app/handlers/admin/payments.py b/app/handlers/admin/payments.py index 682ce7a1..d385f532 100644 --- a/app/handlers/admin/payments.py +++ b/app/handlers/admin/payments.py @@ -822,9 +822,9 @@ async def export_payments( 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( document=BufferedInputFile(file_bytes, filename=filename), diff --git a/app/handlers/admin/promo_offers.py b/app/handlers/admin/promo_offers.py index 38ea47eb..3a38dfb2 100644 --- a/app/handlers/admin/promo_offers.py +++ b/app/handlers/admin/promo_offers.py @@ -4,7 +4,7 @@ import asyncio import html import re from collections.abc import Sequence -from datetime import datetime +from datetime import UTC, datetime import structlog from aiogram import Dispatcher, F, types @@ -260,7 +260,7 @@ def _format_offer_remaining(offer, texts) -> str: if not offer.expires_at: 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: 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(texts.t('ADMIN_PROMO_OFFER_SEND_USER_NO_SUBSCRIPTION', '💳 Подписка отсутствует')) - now = datetime.utcnow() + now = datetime.now(UTC) percent = 0 try: percent = int(getattr(user, 'promo_offer_discount_percent', 0) or 0) @@ -1772,7 +1772,7 @@ async def show_selected_user_details( ) if subscription: - now = datetime.utcnow() + now = datetime.now(UTC) result = await db.execute( select(SubscriptionTemporaryAccess) .options(selectinload(SubscriptionTemporaryAccess.offer)) diff --git a/app/handlers/admin/promocodes.py b/app/handlers/admin/promocodes.py index 15ee323c..60c6ff55 100644 --- a/app/handlers/admin/promocodes.py +++ b/app/handlers/admin/promocodes.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta import structlog 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 if expiry_days > 0: - valid_until = datetime.utcnow() + timedelta(days=expiry_days) + valid_until = datetime.now(UTC) + timedelta(days=expiry_days) type_map = { 'balance': PromoCodeType.BALANCE, @@ -803,7 +803,7 @@ async def process_discount_hours(message: types.Message, db_user: User, state: F valid_until = None if expiry_days > 0: - valid_until = datetime.utcnow() + timedelta(days=expiry_days) + valid_until = datetime.now(UTC) + timedelta(days=expiry_days) # Создаем DISCOUNT промокод # balance_bonus_kopeks = процент скидки (НЕ копейки!) @@ -877,7 +877,7 @@ async def handle_edit_expiry(message: types.Message, db_user: User, state: FSMCo valid_until = None 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) diff --git a/app/handlers/admin/referrals.py b/app/handlers/admin/referrals.py index e854d277..2bc4c078 100644 --- a/app/handlers/admin/referrals.py +++ b/app/handlers/admin/referrals.py @@ -1,5 +1,6 @@ import datetime import json +from datetime import UTC import structlog 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: 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""" 🤝 Реферальная статистика @@ -114,7 +115,7 @@ async def show_referral_statistics(callback: types.CallbackQuery, db_user: User, except Exception as e: 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""" 🤝 Реферальная статистика @@ -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]: """Возвращает начальную и конечную даты для заданного периода.""" - now = datetime.datetime.now() + now = datetime.datetime.now(UTC) today = now.replace(hour=0, minute=0, second=0, microsecond=0) if period == 'today': @@ -1153,9 +1154,9 @@ async def sync_referrals_with_contest( await callback.answer('🏆 Синхронизирую с конкурсами...') 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']) diff --git a/app/handlers/admin/remnawave.py b/app/handlers/admin/remnawave.py index d374e650..b0bd5277 100644 --- a/app/handlers/admin/remnawave.py +++ b/app/handlers/admin/remnawave.py @@ -1,5 +1,5 @@ import math -from datetime import datetime +from datetime import UTC, datetime from typing import Any import structlog @@ -1041,7 +1041,7 @@ async def show_system_stats(callback: types.CallbackQuery, db_user: User, db: As """ text += f""" -🕒 Обновлено: {format_datetime(stats.get('last_updated', datetime.now()))} +🕒 Обновлено: {format_datetime(stats.get('last_updated', datetime.now(UTC)))} """ 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): text += f'{i}. {node_name}: {format_bytes(total_bytes)}\n' - text += f'\n🕒 Обновлено: {format_datetime(datetime.now())}' + text += f'\n🕒 Обновлено: {format_datetime(datetime.now(UTC))}' keyboard = [ [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: from datetime import datetime, timedelta - end_date = datetime.now() + end_date = datetime.now(UTC) start_date = end_date - timedelta(days=7) node_usage = await remnawave_service.get_node_user_usage_by_range(node_uuid, start_date, end_date) diff --git a/app/handlers/admin/statistics.py b/app/handlers/admin/statistics.py index 4d61c167..a1bf162b 100644 --- a/app/handlers/admin/statistics.py +++ b/app/handlers/admin/statistics.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta import structlog 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'] 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""" 👥 Статистика пользователей @@ -89,7 +89,7 @@ async def show_subscriptions_statistics(callback: types.CallbackQuery, db_user: total_subs = stats['total_subscriptions'] 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""" 📱 Статистика подписок @@ -133,12 +133,12 @@ async def show_subscriptions_statistics(callback: types.CallbackQuery, db_user: @admin_required @error_handler 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_stats = await get_transactions_statistics(db, month_start, now) all_time_stats = await get_transactions_statistics(db) - current_time = format_datetime(datetime.utcnow()) + current_time = format_datetime(datetime.now(UTC)) text = f""" 💰 Статистика доходов @@ -189,7 +189,7 @@ async def show_revenue_statistics(callback: types.CallbackQuery, db_user: User, @error_handler async def show_referral_statistics(callback: types.CallbackQuery, db_user: User, db: AsyncSession): stats = await get_referral_statistics(db) - current_time = format_datetime(datetime.utcnow()) + current_time = format_datetime(datetime.now(UTC)) avg_per_referrer = 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) 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) 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 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) 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] elif period == 'today': - today = datetime.utcnow().date() + today = datetime.now(UTC).date() revenue_data = [r for r in revenue_data if r['date'] == today] total_revenue = sum(r['amount_kopeks'] for r in revenue_data) diff --git a/app/handlers/admin/system_logs.py b/app/handlers/admin/system_logs.py index 0eb24ce1..8957bd73 100644 --- a/app/handlers/admin/system_logs.py +++ b/app/handlers/admin/system_logs.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import UTC, datetime from html import escape from pathlib import Path @@ -47,7 +47,7 @@ def _build_logs_message(log_path: Path) -> str: total_length = len(content) stats = log_path.stat() - updated_at = datetime.fromtimestamp(stats.st_mtime) + updated_at = datetime.fromtimestamp(stats.st_mtime, tz=UTC) if not content: preview_text = 'Лог-файл пуст.' @@ -128,7 +128,7 @@ async def download_system_logs( document = FSInputFile(log_path) 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 = ( f'🧾 Лог-файл {log_path.name}\n📁 Путь: {log_path}\n🕒 Обновлен: {updated_at}' ) diff --git a/app/handlers/admin/tickets.py b/app/handlers/admin/tickets.py index 03c99aa1..ffd06d11 100644 --- a/app/handlers/admin/tickets.py +++ b/app/handlers/admin/tickets.py @@ -1,6 +1,6 @@ import html import time -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta import structlog 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: await message.answer('❌ Введите целое число минут') 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) if ok: await message.answer(f'✅ Пользователь заблокирован на {minutes} минут') @@ -769,7 +769,7 @@ async def handle_admin_block_duration_input(message: types.Message, state: FSMCo await state.clear() 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) if not ok: await message.answer('❌ Ошибка блокировки') diff --git a/app/handlers/admin/users.py b/app/handlers/admin/users.py index c24eae45..8e5e7c45 100644 --- a/app/handlers/admin/users.py +++ b/app/handlers/admin/users.py @@ -1,6 +1,6 @@ import re from dataclasses import dataclass -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from enum import Enum from typing import Any @@ -146,7 +146,7 @@ def _build_user_button_text( if user.balance_kopeks > 0: button_text += f' | 💰 {settings.format_price(user.balance_kopeks)}' 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}д' elif filter_type == UserFilterType.CAMPAIGN: @@ -425,7 +425,7 @@ async def show_users_ready_to_renew( text += 'Нажмите на пользователя для управления:' keyboard = [] - current_time = datetime.utcnow() + current_time = datetime.now(UTC) for user in users_data['users']: 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 - current_time = datetime.utcnow() + current_time = datetime.now(UTC) active_subscription_query = ( select(func.count(Subscription.id)) @@ -859,7 +859,7 @@ async def _render_user_subscription_overview(callback: types.CallbackQuery, db: text += f'Устройства: {subscription.device_limit}\n' if subscription.is_active: - days_left = (subscription.end_date - datetime.utcnow()).days + days_left = (subscription.end_date - datetime.now(UTC)).days text += f'Осталось дней: {days_left}\n' current_squads = subscription.connected_squads or [] @@ -1558,7 +1558,7 @@ async def _update_referral_commission_percent( return False, None user.referral_commission_percent = percent - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) 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 referrals_detail = [] - current_time = datetime.utcnow() + current_time = datetime.now(UTC) for referral in referrals: 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 = 'добавлен' subscription.connected_squads = current_squads - subscription.updated_at = datetime.utcnow() + subscription.updated_at = datetime.now(UTC) await db.commit() await db.refresh(subscription) @@ -3872,7 +3872,7 @@ async def _update_user_devices(db: AsyncSession, user_id: int, devices: int, adm subscription = user.subscription old_devices = subscription.device_limit subscription.device_limit = devices - subscription.updated_at = datetime.utcnow() + subscription.updated_at = datetime.now(UTC) await db.commit() @@ -3916,7 +3916,7 @@ async def _update_user_traffic(db: AsyncSession, user_id: int, traffic_gb: int, subscription = user.subscription old_traffic = subscription.traffic_limit_gb subscription.traffic_limit_gb = traffic_gb - subscription.updated_at = datetime.utcnow() + subscription.updated_at = datetime.now(UTC) 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: try: - from datetime import datetime + from datetime import UTC, datetime from app.database.crud.subscription import get_subscription_by_user_id from app.database.models import SubscriptionStatus @@ -4050,8 +4050,8 @@ async def _activate_user_subscription(db: AsyncSession, user_id: int, admin_id: return False subscription.status = SubscriptionStatus.ACTIVE.value - if subscription.end_date <= datetime.utcnow(): - subscription.end_date = datetime.utcnow() + timedelta(days=1) + if subscription.end_date <= datetime.now(UTC): + subscription.end_date = datetime.now(UTC) + timedelta(days=1) await db.commit() await db.refresh(subscription) @@ -4506,7 +4506,7 @@ async def admin_buy_subscription_execute(callback: types.CallbackQuery, db_user: return if subscription: - current_time = datetime.utcnow() + current_time = datetime.now(UTC) bonus_period = timedelta() 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 'платной' 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: 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.traffic_limit_gb = tariff.traffic_limit_gb 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 diff --git a/app/handlers/balance/heleket.py b/app/handlers/balance/heleket.py index d716580b..002e365b 100644 --- a/app/handlers/balance/heleket.py +++ b/app/handlers/balance/heleket.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import UTC, datetime import structlog from aiogram import types @@ -251,7 +251,7 @@ async def process_heleket_payment_amount( await db.execute( update(payment.__class__) .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() except Exception as error: # pragma: no cover - diagnostics diff --git a/app/handlers/balance/pal24.py b/app/handlers/balance/pal24.py index 5fcbf6ca..376f71a4 100644 --- a/app/handlers/balance/pal24.py +++ b/app/handlers/balance/pal24.py @@ -1,5 +1,5 @@ import html -from datetime import datetime +from datetime import UTC, datetime from typing import Any import structlog @@ -220,7 +220,7 @@ async def _send_pal24_payment_message( await db.execute( update(payment.__class__) .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() except Exception as error: # pragma: no cover - diagnostics diff --git a/app/handlers/balance/wata.py b/app/handlers/balance/wata.py index 4682747d..6b53fd99 100644 --- a/app/handlers/balance/wata.py +++ b/app/handlers/balance/wata.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import UTC, datetime import structlog from aiogram import types @@ -234,7 +234,7 @@ async def process_wata_payment_amount( await db.execute( update(payment.__class__) .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() except Exception as error: # pragma: no cover - diagnostics diff --git a/app/handlers/balance/yookassa.py b/app/handlers/balance/yookassa.py index 74e65c83..92721ae1 100644 --- a/app/handlers/balance/yookassa.py +++ b/app/handlers/balance/yookassa.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import UTC, datetime import structlog from aiogram import types @@ -270,7 +270,7 @@ async def process_yookassa_payment_amount( await db.execute( update(payment.__class__) .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() except Exception as error: # pragma: no cover - диагностический лог @@ -509,7 +509,7 @@ async def process_yookassa_sbp_payment_amount( await db.execute( update(payment.__class__) .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() except Exception as error: # pragma: no cover - диагностический лог diff --git a/app/handlers/contests.py b/app/handlers/contests.py index 11d58e20..33d212b3 100644 --- a/app/handlers/contests.py +++ b/app/handlers/contests.py @@ -1,6 +1,6 @@ """Contest handlers for daily games.""" -from datetime import datetime +from datetime import UTC, datetime import structlog 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: """Check if user exceeds rate limit for contest actions.""" key = f'{user_id}_{action}' - now = datetime.utcnow().timestamp() + now = datetime.now(UTC).timestamp() if key not in _rate_limits: _rate_limits[key] = [] diff --git a/app/handlers/menu.py b/app/handlers/menu.py index 1512891c..4595188b 100644 --- a/app/handlers/menu.py +++ b/app/handlers/menu.py @@ -1,5 +1,5 @@ import html -from datetime import datetime +from datetime import UTC, datetime from decimal import Decimal import structlog @@ -165,7 +165,7 @@ async def show_main_menu( texts = get_texts(db_user.language) - db_user.last_activity = datetime.utcnow() + db_user.last_activity = datetime.now(UTC) await db.commit() 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: return texts.t('SUB_STATUS_NONE', '❌ Отсутствует') - current_time = datetime.utcnow() + current_time = datetime.now(UTC) actual_status = (subscription.actual_status or '').lower() end_date = getattr(subscription, 'end_date', 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) # Если подписка активна — ничего не делаем - 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( texts.t('SUBSCRIPTION_ALREADY_ACTIVE', '✅ Подписка уже активна!'), show_alert=True, diff --git a/app/handlers/polls.py b/app/handlers/polls.py index fef6f778..69022c9b 100644 --- a/app/handlers/polls.py +++ b/app/handlers/polls.py @@ -1,5 +1,5 @@ import asyncio -from datetime import datetime +from datetime import UTC, datetime import structlog from aiogram import Dispatcher, F, types @@ -113,7 +113,7 @@ async def handle_poll_start( return if not response.started_at: - response.started_at = datetime.utcnow() + response.started_at = datetime.now(UTC) await db.commit() index, question = await get_next_question(response) @@ -213,7 +213,7 @@ async def handle_poll_answer( await callback.answer() return - response.completed_at = datetime.utcnow() + response.completed_at = datetime.now(UTC) await db.commit() reward_amount = await reward_user_for_poll(db, response) diff --git a/app/handlers/server_status.py b/app/handlers/server_status.py index 858c8337..07026a4c 100644 --- a/app/handlers/server_status.py +++ b/app/handlers/server_status.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import UTC, datetime import structlog from aiogram import Dispatcher, F, types @@ -106,7 +106,7 @@ def _build_status_message( offline=len(offline_servers), ) - updated_at = datetime.now().strftime('%H:%M:%S') + updated_at = datetime.now(UTC).strftime('%H:%M:%S') lines.extend( [ diff --git a/app/handlers/simple_subscription.py b/app/handlers/simple_subscription.py index 11f5f7cb..1990ce1a 100644 --- a/app/handlers/simple_subscription.py +++ b/app/handlers/simple_subscription.py @@ -1,7 +1,7 @@ """Обработчики для простой покупки подписки.""" import html -from datetime import datetime +from datetime import UTC, datetime from typing import Any import structlog @@ -137,7 +137,7 @@ async def start_simple_subscription_purchase( elif getattr(current_subscription, 'is_trial', False): # Это тестовая подписка 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: days_left = 0 key = ( diff --git a/app/handlers/start.py b/app/handlers/start.py index 7a42b6d7..829d9433 100644 --- a/app/handlers/start.py +++ b/app/handlers/start.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import UTC, datetime import structlog 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) profile_updated = True - user.last_activity = datetime.utcnow() + user.last_activity = datetime.now(UTC) if profile_updated: - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) await db.commit() await db.refresh(user) 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.first_name = message.from_user.first_name user.last_name = message.from_user.last_name - user.updated_at = datetime.utcnow() - user.last_activity = datetime.utcnow() + user.updated_at = datetime.now(UTC) + user.last_activity = datetime.now(UTC) 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.has_had_paid_subscription = False - from datetime import datetime + from datetime import UTC, datetime - existing_user.updated_at = datetime.utcnow() - existing_user.last_activity = datetime.utcnow() + existing_user.updated_at = datetime.now(UTC) + existing_user.last_activity = datetime.now(UTC) await db.commit() 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: existing_user.referred_by_id = referrer_id - from datetime import datetime + from datetime import UTC, datetime - existing_user.updated_at = datetime.utcnow() - existing_user.last_activity = datetime.utcnow() + existing_user.updated_at = datetime.now(UTC) + existing_user.last_activity = datetime.now(UTC) await db.commit() 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.has_had_paid_subscription = False - from datetime import datetime + from datetime import UTC, datetime - existing_user.updated_at = datetime.utcnow() - existing_user.last_activity = datetime.utcnow() + existing_user.updated_at = datetime.now(UTC) + existing_user.last_activity = datetime.now(UTC) await db.commit() 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: existing_user.referred_by_id = referrer_id - from datetime import datetime + from datetime import UTC, datetime - existing_user.updated_at = datetime.utcnow() - existing_user.last_activity = datetime.utcnow() + existing_user.updated_at = datetime.now(UTC) + existing_user.last_activity = datetime.now(UTC) await db.commit() await db.refresh(existing_user, ['subscription']) @@ -1661,11 +1661,11 @@ def _get_subscription_status(user, texts): subscription = user.subscription 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_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': return texts.t('SUB_STATUS_DISABLED', '⚫ Отключена') @@ -1927,7 +1927,7 @@ async def required_sub_channel_check( subscription = user.subscription if subscription.is_trial and subscription.status == SubscriptionStatus.DISABLED.value: subscription.status = SubscriptionStatus.ACTIVE.value - subscription.updated_at = datetime.utcnow() + subscription.updated_at = datetime.now(UTC) await db.commit() await db.refresh(subscription) logger.info( diff --git a/app/handlers/subscription/countries.py b/app/handlers/subscription/countries.py index 3e892cad..92244ff1 100644 --- a/app/handlers/subscription/countries.py +++ b/app/handlers/subscription/countries.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import UTC, datetime from aiogram import types 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.updated_at = datetime.utcnow() + subscription.updated_at = datetime.now(UTC) await db.commit() subscription_service = SubscriptionService() @@ -900,7 +900,7 @@ async def confirm_add_countries_to_subscription( ) subscription.connected_squads = selected_countries - subscription.updated_at = datetime.utcnow() + subscription.updated_at = datetime.now(UTC) await db.commit() subscription_service = SubscriptionService() diff --git a/app/handlers/subscription/devices.py b/app/handlers/subscription/devices.py index df12930a..f7c0cd99 100644 --- a/app/handlers/subscription/devices.py +++ b/app/handlers/subscription/devices.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import UTC, datetime from aiogram import types 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: # Для суточных тарифов считаем по дням (как в кабинете) - now = datetime.utcnow() + now = datetime.now(UTC) days_left = max(1, (subscription.end_date - now).days) 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.updated_at = datetime.utcnow() + subscription.updated_at = datetime.now(UTC) await db.commit() @@ -1093,7 +1093,7 @@ async def confirm_add_devices(callback: types.CallbackQuery, db_user: User, db: if is_daily_tariff: # Для суточных тарифов считаем по дням (как в кабинете) - now = datetime.utcnow() + now = datetime.now(UTC) days_left = max(1, (subscription.end_date - now).days) period_hint_days = days_left diff --git a/app/handlers/subscription/pricing.py b/app/handlers/subscription/pricing.py index f491160d..a4dc60a1 100644 --- a/app/handlers/subscription/pricing.py +++ b/app/handlers/subscription/pricing.py @@ -446,13 +446,13 @@ async def get_subscription_info_text(subscription, texts, db_user, db: AsyncSess # Отображаем докупленный трафик 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 app.database.models import TrafficPurchase - now = datetime.utcnow() + now = datetime.now(UTC) purchases_query = ( sql_select(TrafficPurchase) .where(TrafficPurchase.subscription_id == subscription.id) diff --git a/app/handlers/subscription/promo.py b/app/handlers/subscription/promo.py index e3524b78..71b7f6ef 100644 --- a/app/handlers/subscription/promo.py +++ b/app/handlers/subscription/promo.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from typing import Any from aiogram import types @@ -126,7 +126,7 @@ async def claim_discount_offer( ) return - now = datetime.utcnow() + now = datetime.now(UTC) if offer.claimed_at is not None: await callback.answer( texts.get('DISCOUNT_CLAIM_ALREADY', 'ℹ️ Скидка уже была активирована'), diff --git a/app/handlers/subscription/purchase.py b/app/handlers/subscription/purchase.py index 5793730d..1247b1f8 100644 --- a/app/handlers/subscription/purchase.py +++ b/app/handlers/subscription/purchase.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from typing import Any 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(db_user) - current_time = datetime.utcnow() + current_time = datetime.now(UTC) if subscription.status == 'expired' or subscription.end_date <= current_time: actual_status = 'expired' @@ -348,10 +348,10 @@ async def show_subscription_info(callback: types.CallbackQuery, db_user: User, d tariff_info_lines.append('⏸️ Подписка приостановлена') # Показываем оставшееся время даже при паузе if last_charge: - from datetime import timedelta + from datetime import UTC, timedelta next_charge = last_charge + timedelta(hours=24) - now = datetime.utcnow() + now = datetime.now(UTC) if next_charge > now: time_until = next_charge - now 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('💤 Списание приостановлено') elif last_charge: - from datetime import timedelta + from datetime import UTC, timedelta next_charge = last_charge + timedelta(hours=24) - now = datetime.utcnow() + now = datetime.now(UTC) if 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 - now = datetime.utcnow() + now = datetime.now(UTC) purchases_query = ( sql_select(TrafficPurchase) .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) return - current_time = datetime.utcnow() + current_time = datetime.now(UTC) if subscription.end_date > current_time: 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 was_trial_conversion = False - current_time = datetime.utcnow() + current_time = datetime.now(UTC) if existing_subscription: logger.info('Обновляем существующую подписку пользователя', telegram_id=db_user.telegram_id) @@ -4311,7 +4311,7 @@ async def _extend_existing_subscription( 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.user import subtract_user_balance @@ -4408,7 +4408,7 @@ async def _extend_existing_subscription( return # Обновляем параметры подписки - current_time = datetime.utcnow() + current_time = datetime.now(UTC) old_end_date = current_subscription.end_date # Обновляем параметры в зависимости от типа текущей подписки diff --git a/app/handlers/subscription/tariff_purchase.py b/app/handlers/subscription/tariff_purchase.py index 97c926d7..26fc6e75 100644 --- a/app/handlers/subscription/tariff_purchase.py +++ b/app/handlers/subscription/tariff_purchase.py @@ -1251,7 +1251,7 @@ async def confirm_daily_tariff_purchase( state: FSMContext, ): """Подтверждает покупку суточного тарифа.""" - from datetime import datetime + from datetime import UTC, datetime tariff_id = int(callback.data.split(':')[1]) 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.is_trial = 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 день - 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 @@ -1344,7 +1344,7 @@ async def confirm_daily_tariff_purchase( tariff_id=tariff.id, ) # Устанавливаем время последнего списания - subscription.last_daily_charge_at = datetime.utcnow() + subscription.last_daily_charge_at = datetime.now(UTC) subscription.is_daily_paused = False await db.commit() 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) days_warning = '' 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) if remaining_days > 1: days_warning = f'\n\n⚠️ Внимание! У вас осталось {remaining_days} дн. подписки.\nПри смене на суточный тариф они будут утеряны!' @@ -2103,7 +2103,7 @@ async def select_tariff_switch_period( state: FSMContext, ): """Обрабатывает выбор периода для переключения тарифа.""" - from datetime import datetime + from datetime import UTC, datetime parts = callback.data.split(':') 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) 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} дней' @@ -2361,7 +2361,7 @@ async def confirm_daily_tariff_switch( state: FSMContext, ): """Подтверждает смену на суточный тариф.""" - from datetime import datetime + from datetime import UTC, datetime tariff_id = int(callback.data.split(':')[1]) tariff = await get_tariff_by_id(db, tariff_id) @@ -2426,9 +2426,9 @@ async def confirm_daily_tariff_switch( subscription.status = 'active' subscription.is_trial = False # Сбрасываем триальный статус subscription.is_daily_paused = False - subscription.last_daily_charge_at = datetime.utcnow() + subscription.last_daily_charge_at = datetime.now(UTC) # Для суточного тарифа ставим срок на 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 @@ -2686,7 +2686,7 @@ async def show_instant_switch_list( state: FSMContext, ): """Показывает список тарифов для мгновенного переключения.""" - from datetime import datetime + from datetime import UTC, datetime texts = get_texts(db_user.language) await state.clear() @@ -2710,7 +2710,7 @@ async def show_instant_switch_list( # Рассчитываем оставшиеся дни remaining_days = 0 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: await callback.message.edit_text( @@ -2767,7 +2767,7 @@ async def preview_instant_switch( state: FSMContext, ): """Показывает превью мгновенного переключения тарифа.""" - from datetime import datetime + from datetime import UTC, datetime tariff_id = int(callback.data.split(':')[1]) new_tariff = await get_tariff_by_id(db, tariff_id) @@ -2794,7 +2794,7 @@ async def preview_instant_switch( return 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) @@ -2925,7 +2925,7 @@ async def confirm_instant_switch( state: FSMContext, ): """Подтверждает мгновенное переключение тарифа.""" - from datetime import datetime, timedelta + from datetime import UTC, datetime, timedelta tariff_id = int(callback.data.split(':')[1]) new_tariff = await get_tariff_by_id(db, tariff_id) @@ -3013,10 +3013,10 @@ async def confirm_instant_switch( 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_daily_paused = False - subscription.last_daily_charge_at = datetime.utcnow() + subscription.last_daily_charge_at = datetime.now(UTC) await db.commit() await db.refresh(subscription) diff --git a/app/handlers/subscription/traffic.py b/app/handlers/subscription/traffic.py index 38734721..7c6f4f19 100644 --- a/app/handlers/subscription/traffic.py +++ b/app/handlers/subscription/traffic.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import UTC, datetime from aiogram import types from aiogram.fsm.context import FSMContext @@ -303,7 +303,7 @@ async def confirm_reset_traffic(callback: types.CallbackQuery, db_user: User, db return subscription.traffic_used_gb = 0.0 - subscription.updated_at = datetime.utcnow() + subscription.updated_at = datetime.now(UTC) await db.commit() 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)) subscription.purchased_traffic_gb = 0 subscription.traffic_reset_at = None # Сбрасываем дату сброса трафика - subscription.updated_at = datetime.utcnow() + subscription.updated_at = datetime.now(UTC) await db.commit() diff --git a/app/keyboards/inline.py b/app/keyboards/inline.py index 95b47b23..839a7ed6 100644 --- a/app/keyboards/inline.py +++ b/app/keyboards/inline.py @@ -68,7 +68,7 @@ async def get_main_menu_keyboard_async( subscription_days_left = subscription.days_left elif hasattr(subscription, 'end_date') and subscription.end_date: # Fallback: вычисляем вручную, используя UTC - now_utc = datetime.now(UTC).replace(tzinfo=None) + now_utc = datetime.now(UTC) days_left = (subscription.end_date - now_utc).days 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: - now_utc = datetime.now(UTC).replace(tzinfo=None) + now_utc = datetime.now(UTC) registration_days = (now_utc - user.created_at).days # ID промо-группы @@ -2033,9 +2033,9 @@ def get_change_devices_keyboard( # Для суточных тарифов считаем по дням, для обычных - по месяцам if is_daily_tariff and subscription_end_date: # Суточный тариф: цена за оставшиеся дни (обычно 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 / 30 (как в кабинете) price_multiplier = days_left / 30 diff --git a/app/middlewares/auth.py b/app/middlewares/auth.py index 12c0aed5..6d6e280a 100644 --- a/app/middlewares/auth.py +++ b/app/middlewares/auth.py @@ -1,6 +1,6 @@ import asyncio from collections.abc import Awaitable, Callable -from datetime import datetime +from datetime import UTC, datetime from typing import Any import structlog @@ -194,10 +194,10 @@ class AuthMiddleware(BaseMiddleware): ) profile_updated = True - db_user.last_activity = datetime.utcnow() + db_user.last_activity = datetime.now(UTC) if profile_updated: - db_user.updated_at = datetime.utcnow() + db_user.updated_at = datetime.now(UTC) logger.info('💾 [Middleware] Профиль пользователя обновлен в middleware', user_id=user.id) if db_user.remnawave_uuid: diff --git a/app/middlewares/channel_checker.py b/app/middlewares/channel_checker.py index f6be0023..e9120fcb 100644 --- a/app/middlewares/channel_checker.py +++ b/app/middlewares/channel_checker.py @@ -1,5 +1,5 @@ from collections.abc import Awaitable, Callable -from datetime import datetime +from datetime import UTC, datetime from typing import Any import redis.asyncio as aioredis @@ -404,7 +404,7 @@ class ChannelCheckerMiddleware(BaseMiddleware): return # Проверяем что подписка ещё не истекла - if subscription.end_date and subscription.end_date <= datetime.utcnow(): + if subscription.end_date and subscription.end_date <= datetime.now(UTC): return # Реактивируем в БД diff --git a/app/middlewares/subscription_checker.py b/app/middlewares/subscription_checker.py index 58b16226..db1f3b8d 100644 --- a/app/middlewares/subscription_checker.py +++ b/app/middlewares/subscription_checker.py @@ -1,5 +1,5 @@ from collections.abc import Awaitable, Callable -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from typing import Any import structlog @@ -37,7 +37,7 @@ class SubscriptionStatusMiddleware(BaseMiddleware): if db and user and user.subscription: try: - current_time = datetime.utcnow() + current_time = datetime.now(UTC) subscription = user.subscription if ( diff --git a/app/services/admin_notification_service.py b/app/services/admin_notification_service.py index bb8b2414..c975b171 100644 --- a/app/services/admin_notification_service.py +++ b/app/services/admin_notification_service.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import UTC, datetime from typing import Any import structlog @@ -275,7 +275,7 @@ class AdminNotificationService: transaction=None, amount_kopeks=charged_amount_kopeks, message='Trial activation', - occurred_at=datetime.utcnow(), + occurred_at=datetime.now(UTC), extra={ 'charged_amount_kopeks': charged_amount_kopeks, 'trial_duration_days': settings.TRIAL_DURATION_DAYS, @@ -356,7 +356,7 @@ class AdminNotificationService: message_lines.append(f'🔗 Реферер: {referrer_info}') message_lines.append('') - message_lines.append(f'⏰ {format_local_datetime(datetime.utcnow(), "%d.%m.%Y %H:%M:%S")}') + message_lines.append(f'⏰ {format_local_datetime(datetime.now(UTC), "%d.%m.%Y %H:%M:%S")}') return await self._send_message('\n'.join(message_lines)) @@ -403,7 +403,7 @@ class AdminNotificationService: transaction=transaction, amount_kopeks=total_amount, 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={ 'period_days': period_days, 'was_trial_conversion': was_trial_conversion, @@ -482,7 +482,7 @@ class AdminNotificationService: message_lines.extend( [ '', - f'{format_local_datetime(datetime.utcnow(), "%d.%m.%Y %H:%M")}', + f'{format_local_datetime(datetime.now(UTC), "%d.%m.%Y %H:%M")}', ] ) @@ -503,7 +503,7 @@ class AdminNotificationService: repo = getattr(settings, 'VERSION_CHECK_REPO', 'fr1ngg/remnawave-bedolaga-telegram-bot') release_url = f'https://github.com/{repo}/releases/tag/{latest_version.tag_name}' 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: header = '🧪 Pre-release' @@ -568,7 +568,7 @@ class AdminNotificationService: 🔄 Следующая попытка через час. ⚙️ Проверьте доступность GitHub API и настройки сети. - ⚙️ Система автоматических обновлений • {format_local_datetime(datetime.utcnow(), '%d.%m.%Y %H:%M:%S')}""" + ⚙️ Система автоматических обновлений • {format_local_datetime(datetime.now(UTC), '%d.%m.%Y %H:%M:%S')}""" return await self._send_message(message) @@ -590,7 +590,7 @@ class AdminNotificationService: payment_method = self._get_payment_method_display(transaction.payment_method) balance_change = user.balance_kopeks - old_balance 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_id_display = self._get_user_identifier_display(user) @@ -883,7 +883,7 @@ class AdminNotificationService: 💰 Баланс после операции: {settings.format_price(current_balance)} -⏰ {format_local_datetime(datetime.utcnow(), '%d.%m.%Y %H:%M:%S')}""" +⏰ {format_local_datetime(datetime.now(UTC), '%d.%m.%Y %H:%M:%S')}""" return await self._send_message(message) @@ -909,7 +909,7 @@ class AdminNotificationService: transaction=None, amount_kopeks=promocode_data.get('balance_bonus_kopeks'), message='Promocode activation', - occurred_at=datetime.utcnow(), + occurred_at=datetime.now(UTC), extra={ 'code': promocode_data.get('code'), 'type': promocode_data.get('type'), @@ -988,7 +988,7 @@ class AdminNotificationService: '📝 Эффект:', effect_description.strip() or '✅ Промокод активирован', '', - f'⏰ {format_local_datetime(datetime.utcnow(), "%d.%m.%Y %H:%M:%S")}', + f'⏰ {format_local_datetime(datetime.now(UTC), "%d.%m.%Y %H:%M:%S")}', ] ) @@ -1015,7 +1015,7 @@ class AdminNotificationService: transaction=None, amount_kopeks=None, message='Referral link visit', - occurred_at=datetime.utcnow(), + occurred_at=datetime.now(UTC), extra={ 'campaign_id': campaign.id, 'campaign_name': campaign.name, @@ -1067,7 +1067,7 @@ class AdminNotificationService: message_lines.extend( [ '', - f'⏰ {format_local_datetime(datetime.utcnow(), "%d.%m.%Y %H:%M:%S")}', + f'⏰ {format_local_datetime(datetime.now(UTC), "%d.%m.%Y %H:%M:%S")}', ] ) @@ -1096,7 +1096,7 @@ class AdminNotificationService: subscription=None, transaction=None, message='Promo group change', - occurred_at=datetime.utcnow(), + occurred_at=datetime.now(UTC), extra={ 'old_group_id': getattr(old_group, 'id', None), 'old_group_name': getattr(old_group, 'name', None), @@ -1157,7 +1157,7 @@ class AdminNotificationService: [ '', f'💰 Баланс пользователя: {settings.format_price(user.balance_kopeks)}', - f'⏰ {format_local_datetime(datetime.utcnow(), "%d.%m.%Y %H:%M:%S")}', + f'⏰ {format_local_datetime(datetime.now(UTC), "%d.%m.%Y %H:%M:%S")}', ] ) @@ -1325,7 +1325,7 @@ class AdminNotificationService: if details.get('enabled_at'): enabled_at = details['enabled_at'] if isinstance(enabled_at, str): - from datetime import datetime + from datetime import UTC, datetime enabled_at = datetime.fromisoformat(enabled_at) message_parts.append( @@ -1342,7 +1342,7 @@ class AdminNotificationService: if details.get('disabled_at'): disabled_at = details['disabled_at'] if isinstance(disabled_at, str): - from datetime import datetime + from datetime import UTC, datetime disabled_at = datetime.fromisoformat(disabled_at) message_parts.append( @@ -1409,7 +1409,7 @@ class AdminNotificationService: message_parts.append('Автоматический мониторинг API остановлен.') message_parts.append('') - message_parts.append(f'⏰ {format_local_datetime(datetime.utcnow(), "%d.%m.%Y %H:%M:%S")}') + message_parts.append(f'⏰ {format_local_datetime(datetime.now(UTC), "%d.%m.%Y %H:%M:%S")}') message = '\n'.join(message_parts) @@ -1446,7 +1446,7 @@ class AdminNotificationService: if details.get('last_check'): last_check = details['last_check'] if isinstance(last_check, str): - from datetime import datetime + from datetime import UTC, datetime last_check = datetime.fromisoformat(last_check) message_parts.append(f'🕐 Последняя проверка: {format_local_datetime(last_check, "%H:%M:%S")}') @@ -1496,7 +1496,7 @@ class AdminNotificationService: message_parts.append('Панель временно недоступна для обслуживания.') message_parts.append('') - message_parts.append(f'⏰ {format_local_datetime(datetime.utcnow(), "%d.%m.%Y %H:%M:%S")}') + message_parts.append(f'⏰ {format_local_datetime(datetime.now(UTC), "%d.%m.%Y %H:%M:%S")}') message = '\n'.join(message_parts) @@ -1589,7 +1589,7 @@ class AdminNotificationService: message_lines.extend( [ '', - f'{format_local_datetime(datetime.utcnow(), "%d.%m.%Y %H:%M")}', + f'{format_local_datetime(datetime.now(UTC), "%d.%m.%Y %H:%M")}', ] ) @@ -1662,7 +1662,7 @@ class AdminNotificationService: message_lines.extend( [ '', - f'⏰ {format_local_datetime(datetime.utcnow(), "%d.%m.%Y %H:%M:%S")}', + f'⏰ {format_local_datetime(datetime.now(UTC), "%d.%m.%Y %H:%M:%S")}', ] ) diff --git a/app/services/backup_service.py b/app/services/backup_service.py index c75c22f1..d38d4ae2 100644 --- a/app/services/backup_service.py +++ b/app/services/backup_service.py @@ -7,7 +7,7 @@ import shutil import tarfile import tempfile 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 pathlib import Path from typing import Any @@ -262,7 +262,7 @@ class BackupService: return default_hours, default_minutes 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() next_run = reference.replace(hour=hours, minute=minutes, second=0, microsecond=0) @@ -319,7 +319,7 @@ class BackupService: 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' filename = f'backup_{timestamp}{archive_suffix}' backup_path = self.backup_dir / filename @@ -337,7 +337,7 @@ class BackupService: metadata = { 'format_version': self.archive_format_version, - 'timestamp': datetime.utcnow().isoformat(), + 'timestamp': datetime.now(UTC).isoformat(), 'database_type': 'postgresql' if settings.is_postgresql() else 'sqlite', 'backup_type': 'full', 'tables_count': overview.get('tables_count', 0), @@ -525,7 +525,7 @@ class BackupService: dump_path = staging_dir / 'database.json' dump_structure = { 'metadata': { - 'timestamp': datetime.utcnow().isoformat(), + 'timestamp': datetime.now(UTC).isoformat(), 'version': 'orm-1.0', 'database_type': 'postgresql', 'tables_count': tables_count, @@ -1171,10 +1171,10 @@ class BackupService: if 'T' in value: processed_data[key] = datetime.fromisoformat(value.replace('Z', '+00:00')) 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: 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): try: processed_data[key] = dt_time.fromisoformat(value) @@ -1494,7 +1494,7 @@ class BackupService: snapshots['app_config'] = { 'path': str(path_obj), '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) except Exception as e: @@ -1557,7 +1557,9 @@ class BackupService: backup_info = { 'filename': backup_file.name, '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('database', {}).get('tables_count', 0) ), @@ -1583,7 +1585,7 @@ class BackupService: { 'filename': backup_file.name, 'filepath': str(backup_file), - 'timestamp': datetime.fromtimestamp(file_stats.st_mtime).isoformat(), + 'timestamp': datetime.fromtimestamp(file_stats.st_mtime, tz=UTC).isoformat(), 'tables_count': '?', 'total_records': '?', 'compressed': backup_file.suffix == '.gz', @@ -1683,7 +1685,7 @@ class BackupService: while True: try: - now = datetime.now() + now = datetime.now(UTC) delay = (next_run - now).total_seconds() if delay > 0: @@ -1713,7 +1715,7 @@ class BackupService: break except Exception as 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): try: @@ -1728,7 +1730,7 @@ class BackupService: if file_path: notification_text += f'\n📁 {Path(file_path).name}' - notification_text += f'\n\n⏰ {datetime.now().strftime("%d.%m.%Y %H:%M:%S")}' + notification_text += f'\n\n⏰ {datetime.now(UTC).strftime("%d.%m.%Y %H:%M:%S")}' try: from app.services.admin_notification_service import AdminNotificationService @@ -1762,7 +1764,7 @@ class BackupService: caption = '📦 Резервная копия\n\n' if temp_zip_path: caption += '🔐 Архив защищён паролем\n\n' - caption += f'⏰ {datetime.now().strftime("%d.%m.%Y %H:%M:%S")}' + caption += f'⏰ {datetime.now(UTC).strftime("%d.%m.%Y %H:%M:%S")}' send_kwargs = { 'chat_id': chat_id, diff --git a/app/services/blacklist_service.py b/app/services/blacklist_service.py index a9fc7026..789decc0 100644 --- a/app/services/blacklist_service.py +++ b/app/services/blacklist_service.py @@ -5,7 +5,7 @@ import asyncio import time -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta import aiohttp import structlog @@ -122,7 +122,7 @@ class BlacklistService: ) self.blacklist_data = blacklist_data - self.last_update = datetime.utcnow() + self.last_update = datetime.now(UTC) self._check_cache.clear() logger.info('Черный список успешно обновлен. Найдено записей', blacklist_data_count=len(blacklist_data)) return True @@ -164,7 +164,7 @@ class BlacklistService: # Если черный список пуст или устарел, обновляем его interval_hours = self.get_blacklist_update_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() # Проверяем по Telegram ID @@ -197,7 +197,7 @@ class BlacklistService: """ interval_hours = self.get_blacklist_update_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() return self.blacklist_data.copy() diff --git a/app/services/broadcast_service.py b/app/services/broadcast_service.py index a2e3aee5..4999f866 100644 --- a/app/services/broadcast_service.py +++ b/app/services/broadcast_service.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio from dataclasses import dataclass -from datetime import datetime +from datetime import UTC, datetime from typing import TYPE_CHECKING import structlog @@ -467,7 +467,7 @@ class BroadcastService: broadcast.status = status if update_completed_at: - broadcast.completed_at = datetime.utcnow() + broadcast.completed_at = datetime.now(UTC) await session.commit() return @@ -865,7 +865,7 @@ class EmailBroadcastService: broadcast.status = status if update_completed_at: - broadcast.completed_at = datetime.utcnow() + broadcast.completed_at = datetime.now(UTC) await session.commit() return diff --git a/app/services/contest_rotation_service.py b/app/services/contest_rotation_service.py index 105f9f8f..460da7d0 100644 --- a/app/services/contest_rotation_service.py +++ b/app/services/contest_rotation_service.py @@ -221,8 +221,8 @@ class ContestRotationService: continue # Convert to UTC for storage - starts_at_utc = starts_at_local.astimezone(UTC).replace(tzinfo=None) - ends_at_utc = ends_at_local.astimezone(UTC).replace(tzinfo=None) + starts_at_utc = starts_at_local.astimezone(UTC) + ends_at_utc = ends_at_local.astimezone(UTC) # Анонс перед созданием раунда await self._announce_round_start(tpl, starts_at_local, ends_at_local) diff --git a/app/services/daily_subscription_service.py b/app/services/daily_subscription_service.py index 80134753..ed9754ac 100644 --- a/app/services/daily_subscription_service.py +++ b/app/services/daily_subscription_service.py @@ -5,7 +5,7 @@ """ import asyncio -from datetime import datetime +from datetime import UTC, datetime import structlog from aiogram import Bot @@ -290,7 +290,7 @@ class DailySubscriptionService: async with AsyncSessionLocal() as db: try: # Находим все истекшие докупки - now = datetime.utcnow() + now = datetime.now(UTC) query = select(TrafficPurchase).where(TrafficPurchase.expires_at <= now) result = await db.execute(query) expired_purchases = result.scalars().all() @@ -400,7 +400,7 @@ class DailySubscriptionService: subscription.purchased_traffic_gb = max(0, new_purchased) # Проверяем, остались ли активные докупки - now = datetime.utcnow() + now = datetime.now(UTC) remaining_query = ( select(TrafficPurchase) .where(TrafficPurchase.subscription_id == subscription_id) @@ -417,7 +417,7 @@ class DailySubscriptionService: next_expiry = min(p.expires_at for p in remaining_purchases) subscription.traffic_reset_at = next_expiry - subscription.updated_at = datetime.utcnow() + subscription.updated_at = datetime.now(UTC) await db.commit() diff --git a/app/services/event_emitter.py b/app/services/event_emitter.py index 3e254340..64f391f0 100644 --- a/app/services/event_emitter.py +++ b/app/services/event_emitter.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio import json from collections.abc import Callable -from datetime import datetime +from datetime import UTC, datetime from typing import Any import structlog @@ -60,7 +60,7 @@ class EventEmitter: event_data = { 'type': event_type, 'payload': payload, - 'timestamp': str(datetime.utcnow()), + 'timestamp': str(datetime.now(UTC)), } # Вызываем локальные слушатели diff --git a/app/services/maintenance_service.py b/app/services/maintenance_service.py index 987c2699..d7e9c410 100644 --- a/app/services/maintenance_service.py +++ b/app/services/maintenance_service.py @@ -1,6 +1,6 @@ import asyncio from dataclasses import dataclass -from datetime import datetime +from datetime import UTC, datetime from typing import Any import structlog @@ -76,7 +76,7 @@ class MaintenanceService: emoji_map = {'error': '🚨', 'warning': '⚠️', 'success': '✅', 'info': 'ℹ️'} 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} ТЕХНИЧЕСКИЕ РАБОТЫ\n\n{message}\n\n⏰ {timestamp}' return await notification_service._send_message(formatted_message) @@ -135,7 +135,7 @@ class MaintenanceService: return 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.auto_enabled = auto @@ -168,7 +168,7 @@ class MaintenanceService: was_auto = self._status.auto_enabled duration = None 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.enabled_at = None @@ -187,7 +187,7 @@ class MaintenanceService: else: duration_str = f'\n⏱️ Длительность: {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"""Режим технических работ ВЫКЛЮЧЕН 🤖 Автоматически: {'Да' if was_auto else 'Нет'} @@ -252,7 +252,7 @@ class MaintenanceService: return self._status.api_status self._is_checking = True - self._status.last_check = datetime.utcnow() + self._status.last_check = datetime.now(UTC) auth_params = settings.get_remnawave_auth_params() base_url = (auth_params.get('base_url') or '').strip() @@ -356,7 +356,7 @@ API снова отвечает на запросы.""", logger.error('Ошибка проверки API', error=e) 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( f"""Ошибка при проверке API Remnawave @@ -442,11 +442,11 @@ API снова отвечает на запросы.""", } async def force_api_check(self) -> dict[str, Any]: - start_time = datetime.utcnow() + start_time = datetime.now(UTC) try: api_status = await self.check_api_status() - end_time = datetime.utcnow() + end_time = datetime.now(UTC) response_time = (end_time - start_time).total_seconds() return { @@ -458,7 +458,7 @@ API снова отвечает на запросы.""", } except Exception as e: - end_time = datetime.utcnow() + end_time = datetime.now(UTC) response_time = (end_time - start_time).total_seconds() return { diff --git a/app/services/menu_layout/stats_service.py b/app/services/menu_layout/stats_service.py index 6f050676..bc1b04be 100644 --- a/app/services/menu_layout/stats_service.py +++ b/app/services/menu_layout/stats_service.py @@ -12,14 +12,6 @@ from app.config import settings 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: """Сервис для сбора и анализа статистики кликов по кнопкам.""" @@ -104,7 +96,7 @@ class MenuLayoutStatsService: days: int = 30, ) -> dict[str, Any]: """Получить статистику кликов по конкретной кнопке.""" - now = _utcnow() + now = datetime.now(UTC) today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) week_ago = now - timedelta(days=7) month_ago = now - timedelta(days=days) @@ -172,7 +164,7 @@ class MenuLayoutStatsService: days: int = 30, ) -> list[dict[str, Any]]: """Получить статистику кликов по дням.""" - start_date = _utcnow() - timedelta(days=days) + start_date = datetime.now(UTC) - timedelta(days=days) # Группировка по дате result = await db.execute( @@ -191,7 +183,7 @@ class MenuLayoutStatsService: days: int = 30, ) -> list[dict[str, Any]]: """Получить статистику по всем кнопкам.""" - now = _utcnow() + now = datetime.now(UTC) today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) week_ago = now - timedelta(days=7) month_ago = now - timedelta(days=days) @@ -237,7 +229,7 @@ class MenuLayoutStatsService: days: int = 30, ) -> 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)) return result.scalar() or 0 @@ -249,7 +241,7 @@ class MenuLayoutStatsService: days: int = 30, ) -> list[dict[str, Any]]: """Получить статистику кликов по типам кнопок.""" - start_date = _utcnow() - timedelta(days=days) + start_date = datetime.now(UTC) - timedelta(days=days) result = await db.execute( select( @@ -279,7 +271,7 @@ class MenuLayoutStatsService: days: int = 30, ) -> list[dict[str, Any]]: """Получить статистику кликов по часам дня.""" - start_date = _utcnow() - timedelta(days=days) + start_date = datetime.now(UTC) - timedelta(days=days) # Используем helper-метод для совместимости с SQLite и PostgreSQL hour_expr = cls._get_hour_expr(ButtonClickLog.clicked_at).label('hour') @@ -311,7 +303,7 @@ class MenuLayoutStatsService: Возвращает 0=понедельник, 6=воскресенье. Поддерживает как PostgreSQL, так и SQLite. """ - start_date = _utcnow() - timedelta(days=days) + start_date = datetime.now(UTC) - timedelta(days=days) # Используем helper-метод для совместимости с SQLite и PostgreSQL weekday_expr = cls._get_weekday_expr(ButtonClickLog.clicked_at).label('weekday') @@ -345,7 +337,7 @@ class MenuLayoutStatsService: days: int = 30, ) -> list[dict[str, Any]]: """Получить топ пользователей по количеству кликов.""" - start_date = _utcnow() - timedelta(days=days) + start_date = datetime.now(UTC) - timedelta(days=days) query = select( ButtonClickLog.user_id, @@ -378,7 +370,7 @@ class MenuLayoutStatsService: previous_days: int = 7, ) -> dict[str, Any]: """Сравнить статистику текущего и предыдущего периода.""" - now = _utcnow() + now = datetime.now(UTC) current_start = now - timedelta(days=current_days) previous_start = current_start - timedelta(days=previous_days) previous_end = current_start diff --git a/app/services/monitoring_service.py b/app/services/monitoring_service.py index 4b399521..472f683c 100644 --- a/app/services/monitoring_service.py +++ b/app/services/monitoring_service.py @@ -1,5 +1,5 @@ import asyncio -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from pathlib import Path from typing import Any @@ -84,7 +84,7 @@ class MonitoringService: self.subscription_service = SubscriptionService() self.bot = bot self._notified_users: set[str] = set() - self._last_cleanup = datetime.utcnow() + self._last_cleanup = datetime.now(UTC) self._sla_task = None async def _send_message_with_logo( @@ -225,7 +225,7 @@ class MonitoringService: db, 'monitoring_cycle_completed', 'Цикл мониторинга успешно завершен', - {'timestamp': datetime.utcnow().isoformat()}, + {'timestamp': datetime.now(UTC).isoformat()}, ) await db.commit() @@ -244,7 +244,7 @@ class MonitoringService: await db.rollback() async def _cleanup_notification_cache(self): - current_time = datetime.utcnow() + current_time = datetime.now(UTC) if (current_time - self._last_cleanup).total_seconds() >= 3600: old_count = len(self._notified_users) @@ -318,7 +318,7 @@ class MonitoringService: ) return None - current_time = datetime.utcnow() + current_time = datetime.now(UTC) is_active = subscription.status == SubscriptionStatus.ACTIVE.value and subscription.end_date > current_time if subscription.status == SubscriptionStatus.ACTIVE.value and subscription.end_date <= current_time: @@ -465,7 +465,7 @@ class MonitoringService: async def _check_trial_expiring_soon(self, db: AsyncSession): try: - threshold_time = datetime.utcnow() + timedelta(hours=2) + threshold_time = datetime.now(UTC) + timedelta(hours=2) result = await db.execute( select(Subscription) @@ -480,7 +480,7 @@ class MonitoringService: Subscription.status == SubscriptionStatus.ACTIVE.value, Subscription.is_trial == True, Subscription.end_date <= threshold_time, - Subscription.end_date > datetime.utcnow(), + Subscription.end_date > datetime.now(UTC), ) ) ) @@ -533,7 +533,7 @@ class MonitoringService: return try: - now = datetime.utcnow() + now = datetime.now(UTC) notifications_allowed = ( NotificationSettingsService.are_notifications_globally_enabled() and NotificationSettingsService.is_trial_channel_unsubscribed_enabled() @@ -650,7 +650,7 @@ class MonitoringService: ) continue subscription.status = SubscriptionStatus.ACTIVE.value - subscription.updated_at = datetime.utcnow() + subscription.updated_at = datetime.now(UTC) await db.commit() await db.refresh(subscription) restored_count += 1 @@ -704,7 +704,7 @@ class MonitoringService: return try: - now = datetime.utcnow() + now = datetime.now(UTC) result = await db.execute( select(Subscription) @@ -826,7 +826,7 @@ class MonitoringService: logger.error('Ошибка проверки напоминаний об истекшей подписке', error=e) async def _get_expiring_paid_subscriptions(self, db: AsyncSession, days_before: int) -> list[Subscription]: - current_time = datetime.utcnow() + current_time = datetime.now(UTC) threshold_date = current_time + timedelta(days=days_before) result = await db.execute( @@ -875,7 +875,7 @@ class MonitoringService: return 0 expires_at = getattr(user, 'promo_offer_discount_expires_at', None) - if expires_at and expires_at <= datetime.utcnow(): + if expires_at and expires_at <= datetime.now(UTC): return 0 return max(0, min(100, percent)) @@ -911,7 +911,7 @@ class MonitoringService: user.promo_offer_discount_percent = 0 user.promo_offer_discount_source = None user.promo_offer_discount_expires_at = None - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) await db.commit() await db.refresh(user) @@ -938,7 +938,7 @@ class MonitoringService: async def _process_autopayments(self, db: AsyncSession): try: - current_time = datetime.utcnow() + current_time = datetime.now(UTC) result = await db.execute( select(Subscription) @@ -1576,7 +1576,7 @@ class MonitoringService: async def _cleanup_inactive_users(self, db: AsyncSession): try: - now = datetime.utcnow() + now = datetime.now(UTC) if now.hour != 3: return @@ -1603,7 +1603,7 @@ class MonitoringService: async def _sync_with_remnawave(self, db: AsyncSession): try: - now = datetime.utcnow() + now = datetime.now(UTC) if now.minute != 0: return @@ -1645,7 +1645,7 @@ class MonitoringService: if not settings.is_admin_notifications_enabled(): return - from datetime import datetime, timedelta + from datetime import UTC, datetime, timedelta try: from app.services.support_settings_service import SupportSettingsService @@ -1654,7 +1654,7 @@ class MonitoringService: except Exception: sla_minutes = max(1, int(getattr(settings, 'SUPPORT_TICKET_SLA_MINUTES', 5))) cooldown_minutes = max(1, int(getattr(settings, 'SUPPORT_TICKET_SLA_REMINDER_COOLDOWN_MINUTES', 15))) - now = datetime.utcnow() + now = datetime.now(UTC) stale_before = now - timedelta(minutes=sla_minutes) cooldown_before = now - timedelta(minutes=cooldown_minutes) @@ -1764,7 +1764,7 @@ class MonitoringService: ) recent_events = recent_events_result.scalars().all() - yesterday = datetime.utcnow() - timedelta(days=1) + yesterday = datetime.now(UTC) - timedelta(days=1) events_24h_result = await db.execute(select(MonitoringLog).where(MonitoringLog.created_at >= yesterday)) events_24h = events_24h_result.scalars().all() @@ -1774,7 +1774,7 @@ class MonitoringService: return { 'is_running': self.is_running, - 'last_update': datetime.utcnow(), + 'last_update': datetime.now(UTC), 'recent_events': [ { 'type': event.event_type, @@ -1796,7 +1796,7 @@ class MonitoringService: logger.error('Ошибка получения статуса мониторинга', error=e) return { 'is_running': self.is_running, - 'last_update': datetime.utcnow(), + 'last_update': datetime.now(UTC), 'recent_events': [], 'stats_24h': {'total_events': 0, 'successful': 0, 'failed': 0, 'success_rate': 0}, } @@ -1919,7 +1919,7 @@ class MonitoringService: if days == 0: result = await db.execute(delete(MonitoringLog)) else: - cutoff_date = datetime.utcnow() - timedelta(days=days) + cutoff_date = datetime.now(UTC) - timedelta(days=days) result = await db.execute(delete(MonitoringLog).where(MonitoringLog.created_at < cutoff_date)) deleted_count = result.rowcount diff --git a/app/services/nalogo_queue_service.py b/app/services/nalogo_queue_service.py index 25d73917..2a91b8ce 100644 --- a/app/services/nalogo_queue_service.py +++ b/app/services/nalogo_queue_service.py @@ -5,7 +5,7 @@ """ import asyncio -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta import structlog from aiogram import Bot @@ -101,7 +101,7 @@ class NalogoQueueService: # Проверяем cooldown (можно пропустить для важных уведомлений) if not skip_cooldown: - now = datetime.now() + now = datetime.now(UTC) if self._last_notification_time: if now - self._last_notification_time < self._notification_cooldown: logger.debug('Уведомление о чеках пропущено (cooldown)') @@ -114,7 +114,7 @@ class NalogoQueueService: text=message, parse_mode='HTML', ) - self._last_notification_time = datetime.now() + self._last_notification_time = datetime.now(UTC) logger.info('Отправлено уведомление о чеках NaloGO') except Exception as error: logger.error('Ошибка отправки уведомления о чеках', error=error) @@ -172,6 +172,8 @@ class NalogoQueueService: if created_at_str: try: operation_time = isoparse(created_at_str) + if operation_time.tzinfo is None: + operation_time = operation_time.replace(tzinfo=UTC) except (ValueError, TypeError) as parse_error: logger.warning( 'Не удалось распарсить created_at', created_at_str=created_at_str, parse_error=parse_error diff --git a/app/services/nalogo_service.py b/app/services/nalogo_service.py index 0cb8be31..d937b816 100644 --- a/app/services/nalogo_service.py +++ b/app/services/nalogo_service.py @@ -1,4 +1,4 @@ -from datetime import date, datetime, timedelta +from datetime import UTC, date, datetime, timedelta from decimal import Decimal from typing import Any @@ -117,7 +117,7 @@ class NaloGoService: 'payment_id': payment_id, 'telegram_user_id': telegram_user_id, 'amount_kopeks': amount_kopeks, - 'created_at': datetime.now().isoformat(), + 'created_at': datetime.now(UTC).isoformat(), 'attempts': 0, } success = await cache.lpush(NALOGO_QUEUE_KEY, receipt_data) @@ -159,7 +159,7 @@ class NaloGoService: 'payment_id': payment_id, 'telegram_user_id': telegram_user_id, 'amount_kopeks': amount_kopeks, - 'created_at': datetime.now().isoformat(), + 'created_at': datetime.now(UTC).isoformat(), 'error': error_message, 'status': 'pending_verification', } @@ -489,13 +489,10 @@ class NaloGoService: from dateutil.parser import isoparse operation_time = isoparse(operation_time_str) + if operation_time.tzinfo is None: + operation_time = operation_time.replace(tzinfo=UTC) - # Убираем timezone для сравнения - if operation_time.tzinfo: - operation_time = operation_time.replace(tzinfo=None) - created_at_naive = created_at.replace(tzinfo=None) if created_at.tzinfo else created_at - - time_diff = abs((operation_time - created_at_naive).total_seconds()) + time_diff = abs((operation_time - created_at).total_seconds()) if time_diff <= time_window_minutes * 60: receipt_uuid = income.get('approvedReceiptUuid', income.get('receiptUuid')) if receipt_uuid: diff --git a/app/services/pal24_service.py b/app/services/pal24_service.py index d14524ac..b9ab7215 100644 --- a/app/services/pal24_service.py +++ b/app/services/pal24_service.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from decimal import Decimal from typing import Any @@ -123,4 +123,4 @@ class Pal24Service: def get_expiration(ttl_seconds: int | None) -> datetime | None: if not ttl_seconds: return None - return datetime.utcnow() + timedelta(seconds=ttl_seconds) + return datetime.now(UTC) + timedelta(seconds=ttl_seconds) diff --git a/app/services/partner_stats_service.py b/app/services/partner_stats_service.py index 2282645e..cf5fcf97 100644 --- a/app/services/partner_stats_service.py +++ b/app/services/partner_stats_service.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from typing import Any import structlog @@ -30,7 +30,7 @@ class PartnerStatsService: user_id: int, ) -> dict[str, Any]: """Получить детальную статистику реферера.""" - now = datetime.utcnow() + now = datetime.now(UTC) today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) week_ago = now - timedelta(days=7) month_ago = now - timedelta(days=30) @@ -137,7 +137,7 @@ class PartnerStatsService: days: int = 30, ) -> list[dict[str, Any]]: """Получить статистику реферера по дням.""" - now = datetime.utcnow() + now = datetime.now(UTC) start_date = now - timedelta(days=days) # Рефералы по дням @@ -196,7 +196,7 @@ class PartnerStatsService: limit: int = 10, ) -> list[dict[str, Any]]: """Получить топ рефералов по доходу для реферера.""" - now = datetime.utcnow() + now = datetime.now(UTC) # Получаем рефералов с их доходами result = await db.execute( @@ -266,7 +266,7 @@ class PartnerStatsService: previous_days: int = 7, ) -> dict[str, Any]: """Сравнить текущий и предыдущий период.""" - now = datetime.utcnow() + now = datetime.now(UTC) current_start = now - timedelta(days=current_days) previous_start = current_start - timedelta(days=previous_days) previous_end = current_start @@ -345,7 +345,7 @@ class PartnerStatsService: days: int = 30, ) -> dict[str, Any]: """Глобальная статистика партнёрской программы.""" - now = datetime.utcnow() + now = datetime.now(UTC) today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) week_ago = now - timedelta(days=7) month_ago = now - timedelta(days=30) @@ -449,7 +449,7 @@ class PartnerStatsService: days: int = 30, ) -> list[dict[str, Any]]: """Глобальная статистика по дням.""" - now = datetime.utcnow() + now = datetime.now(UTC) start_date = now - timedelta(days=days) # Рефералы по дням @@ -501,7 +501,7 @@ class PartnerStatsService: days: int | None = None, ) -> list[dict[str, Any]]: """Получить топ рефереров.""" - now = datetime.utcnow() + now = datetime.now(UTC) start_date = now - timedelta(days=days) if days else None # Подсчёт рефералов и заработков diff --git a/app/services/payment/cloudpayments.py b/app/services/payment/cloudpayments.py index 0f75c54c..cd68a7ff 100644 --- a/app/services/payment/cloudpayments.py +++ b/app/services/payment/cloudpayments.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import datetime +from datetime import UTC, datetime from importlib import import_module from typing import Any @@ -198,7 +198,7 @@ class CloudPaymentsPaymentMixin: payment.transaction_id_cp = transaction_id_cp payment.status = 'completed' payment.is_paid = True - payment.paid_at = datetime.utcnow() + payment.paid_at = datetime.now(UTC) payment.token = token payment.card_first_six = webhook_data.get('card_first_six') payment.card_last_four = webhook_data.get('card_last_four') diff --git a/app/services/payment/common.py b/app/services/payment/common.py index 5e2cff06..3e90d9b2 100644 --- a/app/services/payment/common.py +++ b/app/services/payment/common.py @@ -7,7 +7,7 @@ from __future__ import annotations -from datetime import datetime +from datetime import UTC, datetime from types import SimpleNamespace from typing import Any @@ -61,7 +61,7 @@ class PaymentCommonMixin: ) row = result.one_or_none() if row: - is_active = row.status == 'active' and row.end_date > datetime.utcnow() + is_active = row.status == 'active' and row.end_date > datetime.now(UTC) has_active_subscription = bool(is_active and not row.is_trial) except Exception as db_error: logger.warning( diff --git a/app/services/payment/cryptobot.py b/app/services/payment/cryptobot.py index 2c395450..d98916fd 100644 --- a/app/services/payment/cryptobot.py +++ b/app/services/payment/cryptobot.py @@ -4,7 +4,7 @@ from __future__ import annotations import math from dataclasses import dataclass -from datetime import datetime +from datetime import UTC, datetime from importlib import import_module from typing import Any @@ -168,11 +168,11 @@ class CryptoBotPaymentMixin: paid_at_str = payload.get('paid_at') if paid_at_str: try: - paid_at = datetime.fromisoformat(paid_at_str.replace('Z', '+00:00')).replace(tzinfo=None) + paid_at = datetime.fromisoformat(paid_at_str.replace('Z', '+00:00')) except Exception: - paid_at = datetime.utcnow() + paid_at = datetime.now(UTC) else: - paid_at = datetime.utcnow() + paid_at = datetime.now(UTC) updated_payment = await cryptobot_crud.update_cryptobot_payment_status(db, invoice_id, status, paid_at) @@ -266,7 +266,7 @@ class CryptoBotPaymentMixin: was_first_topup = not user.has_made_first_topup user.balance_kopeks += amount_kopeks - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) referrer_info = format_referrer_info(user) topup_status = '🆕 Первое пополнение' if was_first_topup else '🔄 Пополнение' @@ -698,7 +698,7 @@ class CryptoBotPaymentMixin: paid_at = None if paid_at_str: try: - paid_at = datetime.fromisoformat(paid_at_str.replace('Z', '+00:00')).replace(tzinfo=None) + paid_at = datetime.fromisoformat(paid_at_str.replace('Z', '+00:00')) except Exception: # pragma: no cover - defensive parsing paid_at = None diff --git a/app/services/payment/freekassa.py b/app/services/payment/freekassa.py index 10683c54..e0a62ecc 100644 --- a/app/services/payment/freekassa.py +++ b/app/services/payment/freekassa.py @@ -4,7 +4,7 @@ from __future__ import annotations import json import uuid -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from importlib import import_module from typing import Any @@ -74,7 +74,7 @@ class FreekassaPaymentMixin: currency = settings.FREEKASSA_CURRENCY # Срок действия платежа - expires_at = datetime.utcnow() + timedelta(seconds=settings.FREEKASSA_PAYMENT_TIMEOUT_SECONDS) + expires_at = datetime.now(UTC) + timedelta(seconds=settings.FREEKASSA_PAYMENT_TIMEOUT_SECONDS) # Метаданные metadata = { @@ -290,7 +290,7 @@ class FreekassaPaymentMixin: # Начисляем баланс user.balance_kopeks += payment.amount_kopeks - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) promo_group = user.get_primary_promo_group() subscription = getattr(user, 'subscription', None) diff --git a/app/services/payment/heleket.py b/app/services/payment/heleket.py index 605b1a8b..cf0fa5f2 100644 --- a/app/services/payment/heleket.py +++ b/app/services/payment/heleket.py @@ -83,7 +83,7 @@ class HeleketPaymentMixin: metadata: dict[str, Any] = { 'language': language or settings.DEFAULT_LANGUAGE, - 'created_at': datetime.utcnow().isoformat(), + 'created_at': datetime.now(UTC).isoformat(), } try: @@ -127,7 +127,7 @@ class HeleketPaymentMixin: expires_at: datetime | None = None if expires_at_raw: try: - expires_at = datetime.fromtimestamp(int(expires_at_raw)) + expires_at = datetime.fromtimestamp(int(expires_at_raw), tz=UTC) except (TypeError, ValueError, OSError): expires_at = None @@ -222,14 +222,14 @@ class HeleketPaymentMixin: if paid_at_raw: try: if isinstance(paid_at_raw, (int, float)): - paid_at = datetime.utcfromtimestamp(float(paid_at_raw)) + paid_at = datetime.fromtimestamp(float(paid_at_raw), tz=UTC) else: paid_at = datetime.fromisoformat(str(paid_at_raw).replace('Z', '+00:00')) except (ValueError, TypeError): paid_at = None if paid_at and paid_at.tzinfo is not None: - paid_at = paid_at.astimezone(UTC).replace(tzinfo=None) + paid_at = paid_at.astimezone(UTC) updated_payment = await heleket_crud.update_heleket_payment( db, @@ -332,7 +332,7 @@ class HeleketPaymentMixin: was_first_topup = not user.has_made_first_topup user.balance_kopeks += amount_kopeks - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) await db.commit() await db.refresh(user) diff --git a/app/services/payment/kassa_ai.py b/app/services/payment/kassa_ai.py index d256bb92..ec8df571 100644 --- a/app/services/payment/kassa_ai.py +++ b/app/services/payment/kassa_ai.py @@ -4,7 +4,7 @@ from __future__ import annotations import json import uuid -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from importlib import import_module from typing import Any @@ -79,7 +79,7 @@ class KassaAiPaymentMixin: currency = settings.KASSA_AI_CURRENCY # Срок действия платежа (1 час по умолчанию) - expires_at = datetime.utcnow() + timedelta(hours=1) + expires_at = datetime.now(UTC) + timedelta(hours=1) # Метаданные metadata = { @@ -283,7 +283,7 @@ class KassaAiPaymentMixin: # Начисляем баланс user.balance_kopeks += payment.amount_kopeks - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) promo_group = user.get_primary_promo_group() subscription = getattr(user, 'subscription', None) diff --git a/app/services/payment/pal24.py b/app/services/payment/pal24.py index 7c047d9a..d50ccab9 100644 --- a/app/services/payment/pal24.py +++ b/app/services/payment/pal24.py @@ -3,7 +3,7 @@ from __future__ import annotations import uuid -from datetime import datetime +from datetime import UTC, datetime from importlib import import_module from typing import Any @@ -257,7 +257,7 @@ class Pal24PaymentMixin: payment, status=status, is_paid=True, - paid_at=datetime.utcnow(), + paid_at=datetime.now(UTC), callback_payload=callback, payment_id=payment_id, payment_status=callback.get('Status') or status, @@ -381,7 +381,7 @@ class Pal24PaymentMixin: was_first_topup = not user.has_made_first_topup user.balance_kopeks += payment.amount_kopeks - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) promo_group = user.get_primary_promo_group() subscription = getattr(user, 'subscription', None) @@ -654,7 +654,7 @@ class Pal24PaymentMixin: if is_paid_update is not None and is_paid_update != bool(payment.is_paid): update_kwargs['is_paid'] = is_paid_update if is_paid_update and not payment.paid_at: - update_kwargs.setdefault('paid_at', datetime.utcnow()) + update_kwargs.setdefault('paid_at', datetime.now(UTC)) current_status = payment.status or '' effective_status = update_status or current_status or 'NEW' diff --git a/app/services/payment/platega.py b/app/services/payment/platega.py index cc870bb4..cb0a73ef 100644 --- a/app/services/payment/platega.py +++ b/app/services/payment/platega.py @@ -3,7 +3,7 @@ from __future__ import annotations import uuid -from datetime import datetime +from datetime import UTC, datetime from importlib import import_module from typing import Any @@ -271,7 +271,8 @@ class PlategaPaymentMixin: paid_at_raw = payload.get('paidAt') or payload.get('confirmedAt') if paid_at_raw: try: - paid_at = datetime.fromisoformat(str(paid_at_raw)) + paid_at_parsed = datetime.fromisoformat(str(paid_at_raw)) + paid_at = paid_at_parsed if paid_at_parsed.tzinfo else paid_at_parsed.replace(tzinfo=UTC) except ValueError: paid_at = None @@ -378,7 +379,7 @@ class PlategaPaymentMixin: was_first_topup = not user.has_made_first_topup user.balance_kopeks += payment.amount_kopeks - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) await db.commit() await db.refresh(user) topup_status = '🆕 Первое пополнение' if was_first_topup else '🔄 Пополнение' @@ -518,7 +519,7 @@ class PlategaPaymentMixin: metadata['balance_change'] = { 'old_balance': old_balance, 'new_balance': user.balance_kopeks, - 'credited_at': datetime.utcnow().isoformat(), + 'credited_at': datetime.now(UTC).isoformat(), } metadata['balance_credited'] = True diff --git a/app/services/payment/stars.py b/app/services/payment/stars.py index 62c054ca..434f5ec2 100644 --- a/app/services/payment/stars.py +++ b/app/services/payment/stars.py @@ -7,7 +7,7 @@ from __future__ import annotations from dataclasses import dataclass -from datetime import datetime +from datetime import UTC, datetime from decimal import ROUND_HALF_UP, Decimal from aiogram.types import LabeledPrice @@ -240,7 +240,7 @@ class TelegramStarsMixin: return False if payload_data.period_days is None: - start_point = pending_subscription.start_date or datetime.utcnow() + start_point = pending_subscription.start_date or datetime.now(UTC) end_point = pending_subscription.end_date or start_point computed_days = max(1, (end_point - start_point).days or 0) period_days = max(period_days, computed_days) @@ -392,7 +392,7 @@ class TelegramStarsMixin: # Обновляем баланс в БД. user.balance_kopeks += amount_kopeks - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) promo_group = user.get_primary_promo_group() subscription = getattr(user, 'subscription', None) diff --git a/app/services/payment/wata.py b/app/services/payment/wata.py index 424e85de..e00ee61d 100644 --- a/app/services/payment/wata.py +++ b/app/services/payment/wata.py @@ -3,7 +3,7 @@ from __future__ import annotations import uuid -from datetime import datetime +from datetime import UTC, datetime from importlib import import_module from typing import Any @@ -460,7 +460,7 @@ class WataPaymentMixin: was_first_topup = not user.has_made_first_topup user.balance_kopeks += payment.amount_kopeks - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) await db.commit() user = await payment_module.get_user_by_id(db, user.id) if not user: diff --git a/app/services/payment/yookassa.py b/app/services/payment/yookassa.py index 1eb87683..7fa7811d 100644 --- a/app/services/payment/yookassa.py +++ b/app/services/payment/yookassa.py @@ -6,7 +6,7 @@ from __future__ import annotations -from datetime import datetime +from datetime import UTC, datetime from decimal import Decimal, InvalidOperation from importlib import import_module from typing import TYPE_CHECKING, Any @@ -150,7 +150,7 @@ class YooKassaPaymentMixin: if yookassa_response.get('created_at'): try: dt_with_tz = datetime.fromisoformat(yookassa_response['created_at'].replace('Z', '+00:00')) - yookassa_created_at = dt_with_tz.replace(tzinfo=None) + yookassa_created_at = dt_with_tz except Exception as error: logger.warning('Не удалось распарсить created_at', error=error) yookassa_created_at = None @@ -320,7 +320,7 @@ class YooKassaPaymentMixin: captured_at = None if captured_raw: try: - captured_at = datetime.fromisoformat(str(captured_raw).replace('Z', '+00:00')).replace(tzinfo=None) + captured_at = datetime.fromisoformat(str(captured_raw).replace('Z', '+00:00')) except Exception as parse_error: # pragma: no cover - diagnostic log logger.debug( 'Не удалось распарсить captured_at', captured_raw=captured_raw, parse_error=parse_error @@ -723,7 +723,7 @@ class YooKassaPaymentMixin: was_first_topup = not getattr(user, 'has_made_first_topup', False) user.balance_kopeks += payment.amount_kopeks - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) # Обновляем пользователя с нужными связями, чтобы избежать проблем с ленивой загрузкой from sqlalchemy.orm import selectinload @@ -1132,7 +1132,7 @@ class YooKassaPaymentMixin: await db.execute( update(YooKassaPaymentModel) .where(YooKassaPaymentModel.id == payment.id) - .values(metadata_json=updated_metadata, updated_at=datetime.utcnow()) + .values(metadata_json=updated_metadata, updated_at=datetime.now(UTC)) ) if commit: await db.commit() @@ -1195,7 +1195,7 @@ class YooKassaPaymentMixin: if transaction: try: transaction.receipt_uuid = receipt_uuid - transaction.receipt_created_at = datetime.utcnow() + transaction.receipt_created_at = datetime.now(UTC) await db.commit() logger.debug( 'Чек привязан к транзакции', receipt_uuid=receipt_uuid, transaction_id=transaction.id @@ -1278,9 +1278,7 @@ class YooKassaPaymentMixin: captured_at_raw = event_object.get('captured_at') if captured_at_raw: try: - payment.captured_at = datetime.fromisoformat(captured_at_raw.replace('Z', '+00:00')).replace( - tzinfo=None - ) + payment.captured_at = datetime.fromisoformat(captured_at_raw.replace('Z', '+00:00')) except Exception as error: logger.debug('Не удалось распарсить captured_at', captured_at_raw=captured_at_raw, error=error) @@ -1375,7 +1373,7 @@ class YooKassaPaymentMixin: created_at_raw = event_object.get('created_at') if created_at_raw: try: - yookassa_created_at = datetime.fromisoformat(created_at_raw.replace('Z', '+00:00')).replace(tzinfo=None) + yookassa_created_at = datetime.fromisoformat(created_at_raw.replace('Z', '+00:00')) except Exception as error: # pragma: no cover - диагностический лог logger.debug( 'Не удалось распарсить created_at= для YooKassa', @@ -1458,6 +1456,6 @@ class YooKassaPaymentMixin: return None try: - return datetime.fromisoformat(raw_value.replace('Z', '+00:00')).replace(tzinfo=None) + return datetime.fromisoformat(raw_value.replace('Z', '+00:00')) except Exception: return None diff --git a/app/services/payment_verification_service.py b/app/services/payment_verification_service.py index c368f92c..580d219d 100644 --- a/app/services/payment_verification_service.py +++ b/app/services/payment_verification_service.py @@ -7,7 +7,7 @@ import re from collections import Counter from collections.abc import Iterable from dataclasses import dataclass -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from typing import TYPE_CHECKING, Any import structlog @@ -57,7 +57,7 @@ class PendingPayment: expires_at: datetime | None = None def is_recent(self, max_age: timedelta = PENDING_MAX_AGE) -> bool: - return (datetime.utcnow() - self.created_at) <= max_age + return (datetime.now(UTC) - self.created_at) <= max_age SUPPORTED_MANUAL_CHECK_METHODS: frozenset[PaymentMethod] = frozenset( @@ -715,7 +715,7 @@ async def list_recent_pending_payments( ) -> list[PendingPayment]: """Return pending payments (top-ups) from supported providers within the age window.""" - cutoff = datetime.utcnow() - max_age + cutoff = datetime.now(UTC) - max_age tasks: Iterable[list[PendingPayment]] = ( await _fetch_yookassa_payments(db, cutoff), @@ -746,7 +746,7 @@ async def get_payment_record( ) -> PendingPayment | None: """Load single payment record and normalize it to :class:`PendingPayment`.""" - cutoff = datetime.utcnow() - PENDING_MAX_AGE + cutoff = datetime.now(UTC) - PENDING_MAX_AGE if method == PaymentMethod.PAL24: payment = await db.get(Pal24Payment, local_payment_id) diff --git a/app/services/pinned_message_service.py b/app/services/pinned_message_service.py index d6201683..0b3b05da 100644 --- a/app/services/pinned_message_service.py +++ b/app/services/pinned_message_service.py @@ -1,5 +1,5 @@ import asyncio -from datetime import datetime +from datetime import UTC, datetime import structlog from aiogram import Bot @@ -86,7 +86,7 @@ async def deactivate_active_pinned_message(db: AsyncSession) -> PinnedMessage | return None pinned_message.is_active = False - pinned_message.updated_at = datetime.utcnow() + pinned_message.updated_at = datetime.now(UTC) await db.commit() await db.refresh(pinned_message) logger.info('Деактивировано закрепленное сообщение #', pinned_message_id=pinned_message.id) @@ -276,7 +276,7 @@ async def _mark_pinned_delivery( .where(User.id == user_id) .values( last_pinned_message_id=pinned_message_id, - updated_at=datetime.utcnow(), + updated_at=datetime.now(UTC), ) ) await session.commit() diff --git a/app/services/platega_service.py b/app/services/platega_service.py index 258cadc7..60252a25 100644 --- a/app/services/platega_service.py +++ b/app/services/platega_service.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio import json -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from typing import Any import aiohttp @@ -200,7 +200,7 @@ class PlategaService: try: hours, minutes, seconds = [int(part) for part in expires_in.split(':', 2)] delta = timedelta(hours=hours, minutes=minutes, seconds=seconds) - return datetime.utcnow() + delta + return datetime.now(UTC) + delta except Exception: logger.warning('Failed to parse Platega expiresIn value', expires_in=expires_in) return None diff --git a/app/services/promo_group_assignment.py b/app/services/promo_group_assignment.py index d05341cd..b3d46fb5 100644 --- a/app/services/promo_group_assignment.py +++ b/app/services/promo_group_assignment.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import UTC, datetime import structlog from aiogram import Bot @@ -141,14 +141,14 @@ async def maybe_assign_promo_group_by_total_spent( await sync_user_primary_promo_group(db, user_id) if target_threshold > previous_threshold: user.auto_promo_group_threshold_kopeks = target_threshold - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) await db.commit() await db.refresh(user) return target_group user.auto_promo_group_assigned = True user.auto_promo_group_threshold_kopeks = target_threshold - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) if not already_has_group: # Добавляем новую промогруппу к существующим diff --git a/app/services/promo_offer_service.py b/app/services/promo_offer_service.py index f5455da6..2d0b579b 100644 --- a/app/services/promo_offer_service.py +++ b/app/services/promo_offer_service.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections.abc import Sequence -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta import structlog from sqlalchemy import select @@ -63,7 +63,7 @@ class PromoOfferService: if duration_hours <= 0: duration_hours = 24 - now = datetime.utcnow() + now = datetime.now(UTC) expires_at = now + timedelta(hours=duration_hours) original_connected = set(connected) @@ -132,7 +132,7 @@ class PromoOfferService: return True, newly_added, expires_at, 'ok' async def cleanup_expired_test_access(self, db: AsyncSession) -> int: - now = datetime.utcnow() + now = datetime.now(UTC) result = await db.execute( select(SubscriptionTemporaryAccess) .options( diff --git a/app/services/promocode_service.py b/app/services/promocode_service.py index 4a1ff15f..a601d20f 100644 --- a/app/services/promocode_service.py +++ b/app/services/promocode_service.py @@ -189,7 +189,7 @@ class PromoCodeService: # Обработка DISCOUNT типа (одноразовая скидка) if promocode.type == PromoCodeType.DISCOUNT.value: - from datetime import datetime, timedelta + from datetime import UTC, datetime, timedelta # Проверка на наличие активной скидки current_discount = getattr(user, 'promo_offer_discount_percent', 0) or 0 @@ -197,7 +197,7 @@ class PromoCodeService: # Если есть активная скидка (процент > 0 и срок не истек) if current_discount > 0: - if expires_at is None or expires_at > datetime.utcnow(): + if expires_at is None or expires_at > datetime.now(UTC): logger.warning( '⚠️ Пользователь попытался активировать промокод но у него уже есть активная скидка до', _format_user_log=self._format_user_log(user), @@ -218,7 +218,7 @@ class PromoCodeService: # Устанавливаем срок действия скидки if discount_hours > 0: - user.promo_offer_discount_expires_at = datetime.utcnow() + timedelta(hours=discount_hours) + user.promo_offer_discount_expires_at = datetime.now(UTC) + timedelta(hours=discount_hours) effects.append(f'💸 Получена скидка {discount_percent}% (действует {discount_hours} ч.)') else: # 0 часов = бессрочно до первой покупки @@ -373,16 +373,16 @@ class PromoCodeService: if current_discount <= 0 or not source or not source.startswith('promocode:'): return {'success': False, 'error': 'no_active_discount_promocode'} - from datetime import datetime + from datetime import UTC, datetime expires_at = getattr(user, 'promo_offer_discount_expires_at', None) # Если скидка уже истекла по времени -- тоже нечего деактивировать - if expires_at is not None and expires_at <= datetime.utcnow(): + if expires_at is not None and expires_at <= datetime.now(UTC): # Просто зачистим протухшие данные user.promo_offer_discount_percent = 0 user.promo_offer_discount_source = None user.promo_offer_discount_expires_at = None - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) await db.commit() return {'success': False, 'error': 'discount_already_expired'} @@ -394,14 +394,14 @@ class PromoCodeService: user.promo_offer_discount_percent = 0 user.promo_offer_discount_source = None user.promo_offer_discount_expires_at = None - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) # 2. Откатываем использование промокода (если нашли запись) if promocode and promo_use: await db.delete(promo_use) if promocode.current_uses > 0: promocode.current_uses -= 1 - promocode.updated_at = datetime.utcnow() + promocode.updated_at = datetime.now(UTC) # 3. Если промокод назначал промогруппу -- снимаем её if promocode.promo_group_id: diff --git a/app/services/referral_contest_service.py b/app/services/referral_contest_service.py index 2a315652..4474345c 100644 --- a/app/services/referral_contest_service.py +++ b/app/services/referral_contest_service.py @@ -84,7 +84,7 @@ class ReferralContestService: async with AsyncSessionLocal() as db: contests = await get_contests_for_summaries(db) - now_utc = datetime.utcnow() + now_utc = datetime.now(UTC) for contest in contests: try: @@ -112,7 +112,7 @@ class ReferralContestService: summary_times = self._get_summary_times(contest) for summary_time in summary_times: summary_dt = datetime.combine(now_local.date(), summary_time, tzinfo=tz) - summary_dt_utc = summary_dt.astimezone(UTC).replace(tzinfo=None) + summary_dt_utc = summary_dt.astimezone(UTC) if now_utc < summary_dt_utc: continue @@ -143,7 +143,7 @@ class ReferralContestService: summary_times = self._get_summary_times(contest) summary_time = summary_times[-1] if summary_times else time(hour=12, minute=0) summary_dt = datetime.combine(end_local.date(), summary_time, tzinfo=tz) - summary_dt_utc = summary_dt.astimezone(UTC).replace(tzinfo=None) + summary_dt_utc = summary_dt.astimezone(UTC) if now_utc < contest.end_at: return @@ -166,8 +166,8 @@ class ReferralContestService: tz = self._get_timezone(contest) day_start_local = datetime.combine(target_date, time.min, tzinfo=tz) day_end_local = day_start_local + timedelta(days=1) - day_start_utc = day_start_local.astimezone(UTC).replace(tzinfo=None) - day_end_utc = day_end_local.astimezone(UTC).replace(tzinfo=None) + day_start_utc = day_start_local.astimezone(UTC) + day_end_utc = day_end_local.astimezone(UTC) leaderboard = await get_contest_leaderboard_with_virtual(db, contest.id) virtual_participants = await list_virtual_participants(db, contest.id) @@ -521,7 +521,7 @@ class ReferralContestService: if not user or not user.referred_by_id: return - now_utc = datetime.utcnow() + now_utc = datetime.now(UTC) contests = await get_contests_for_events( db, now_utc, @@ -533,13 +533,9 @@ class ReferralContestService: for contest in contests: try: # Проверяем что реферал зарегистрировался В ПЕРИОД конкурса - user_created_at = ( - user.created_at if user.created_at.tzinfo is None else user.created_at.replace(tzinfo=None) - ) - contest_start = ( - contest.start_at if contest.start_at.tzinfo is None else contest.start_at.replace(tzinfo=None) - ) - contest_end = contest.end_at if contest.end_at.tzinfo is None else contest.end_at.replace(tzinfo=None) + user_created_at = user.created_at + contest_start = contest.start_at + contest_end = contest.end_at if user_created_at < contest_start or user_created_at > contest_end: logger.debug( @@ -582,7 +578,7 @@ class ReferralContestService: if not user or not user.referred_by_id: return - now_utc = datetime.utcnow() + now_utc = datetime.now(UTC) contests = await get_contests_for_events( db, now_utc, @@ -594,13 +590,9 @@ class ReferralContestService: for contest in contests: try: # Проверяем что реферал зарегистрировался В ПЕРИОД конкурса - user_created_at = ( - user.created_at if user.created_at.tzinfo is None else user.created_at.replace(tzinfo=None) - ) - contest_start = ( - contest.start_at if contest.start_at.tzinfo is None else contest.start_at.replace(tzinfo=None) - ) - contest_end = contest.end_at if contest.end_at.tzinfo is None else contest.end_at.replace(tzinfo=None) + user_created_at = user.created_at + contest_start = contest.start_at + contest_end = contest.end_at if user_created_at < contest_start or user_created_at > contest_end: logger.debug( diff --git a/app/services/referral_diagnostics_service.py b/app/services/referral_diagnostics_service.py index 31e540f9..7dcdd7be 100644 --- a/app/services/referral_diagnostics_service.py +++ b/app/services/referral_diagnostics_service.py @@ -9,7 +9,7 @@ import re from dataclasses import dataclass, field -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from pathlib import Path from typing import Optional @@ -305,13 +305,13 @@ class ReferralDiagnosticsService: def _find_log_file(self) -> Path: """Ищет существующий лог-файл, предпочитая свежие.""" - today = datetime.now().date() + today = datetime.now(UTC).date() candidates = [] for path_str in self.LOG_PATHS: path = Path(path_str) if path.exists() and path.stat().st_size > 0: - mtime = datetime.fromtimestamp(path.stat().st_mtime).date() + mtime = datetime.fromtimestamp(path.stat().st_mtime, tz=UTC).date() is_fresh = mtime >= today - timedelta(days=1) candidates.append((path, is_fresh, path.stat().st_mtime)) logger.info('📁 Найден лог: (свежий: )', path=path, is_fresh=is_fresh) @@ -339,7 +339,7 @@ class ReferralDiagnosticsService: async def analyze_today(self, db: AsyncSession) -> DiagnosticReport: """Анализирует реферальные события за сегодня.""" - today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) + today = datetime.now(UTC).replace(hour=0, minute=0, second=0, microsecond=0) tomorrow = today + timedelta(days=1) return await self.analyze_period(db, today, tomorrow) @@ -382,8 +382,8 @@ class ReferralDiagnosticsService: # Парсим весь файл без фильтра по дате # Используем широкий диапазон дат (все время) - start_date = datetime(2000, 1, 1) - end_date = datetime(2100, 1, 1) + start_date = datetime(2000, 1, 1, tzinfo=UTC) + end_date = datetime(2100, 1, 1, tzinfo=UTC) # Временно меняем путь к логу original_log_path = self.log_path @@ -473,7 +473,7 @@ class ReferralDiagnosticsService: timestamp_str, message = match.groups() try: - timestamp = datetime.strptime(timestamp_str, '%Y-%m-%d %H:%M:%S') + timestamp = datetime.strptime(timestamp_str, '%Y-%m-%d %H:%M:%S').replace(tzinfo=UTC) except ValueError: continue @@ -596,14 +596,12 @@ class ReferralDiagnosticsService: - Реферал зарегистрирован в период конкурса - Событие ещё не было добавлено """ - from datetime import datetime - from app.database.crud.referral_contest import add_contest_event, get_contests_for_events if not settings.is_contests_enabled(): return - now_utc = datetime.utcnow() + now_utc = datetime.now(UTC) # Проверяем конкурсы по оплаченным рефералам contests = await get_contests_for_events(db, now_utc, contest_types=['referral_paid']) @@ -611,15 +609,9 @@ class ReferralDiagnosticsService: for contest in contests: try: # Проверяем что реферал зарегистрировался В ПЕРИОД конкурса - user_created_at = ( - referral.created_at - if referral.created_at.tzinfo is None - else referral.created_at.replace(tzinfo=None) - ) - contest_start = ( - contest.start_at if contest.start_at.tzinfo is None else contest.start_at.replace(tzinfo=None) - ) - contest_end = contest.end_at if contest.end_at.tzinfo is None else contest.end_at.replace(tzinfo=None) + user_created_at = referral.created_at + contest_start = contest.start_at + contest_end = contest.end_at if user_created_at < contest_start or user_created_at > contest_end: logger.debug( @@ -650,15 +642,9 @@ class ReferralDiagnosticsService: for contest in reg_contests: try: - user_created_at = ( - referral.created_at - if referral.created_at.tzinfo is None - else referral.created_at.replace(tzinfo=None) - ) - contest_start = ( - contest.start_at if contest.start_at.tzinfo is None else contest.start_at.replace(tzinfo=None) - ) - contest_end = contest.end_at if contest.end_at.tzinfo is None else contest.end_at.replace(tzinfo=None) + user_created_at = referral.created_at + contest_start = contest.start_at + contest_end = contest.end_at if user_created_at < contest_start or user_created_at > contest_end: continue diff --git a/app/services/referral_withdrawal_service.py b/app/services/referral_withdrawal_service.py index 9e2d9fc3..6663a84a 100644 --- a/app/services/referral_withdrawal_service.py +++ b/app/services/referral_withdrawal_service.py @@ -4,7 +4,7 @@ """ import json -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta import structlog from sqlalchemy import func, select @@ -195,8 +195,8 @@ class ReferralWithdrawalService: cooldown_days = settings.REFERRAL_WITHDRAWAL_COOLDOWN_DAYS cooldown_end = last_request.created_at + timedelta(days=cooldown_days) - if datetime.utcnow() < cooldown_end: - days_left = (cooldown_end - datetime.utcnow()).days + 1 + if datetime.now(UTC) < cooldown_end: + days_left = (cooldown_end - datetime.now(UTC)).days + 1 return False, f'Следующий запрос на вывод будет доступен через {days_left} дн.' # Проверяем, нет ли активной заявки @@ -245,7 +245,7 @@ class ReferralWithdrawalService: if referral_ids: # Получаем детальную статистику по каждому рефералу за последний месяц - month_ago = datetime.utcnow() - timedelta(days=30) + month_ago = datetime.now(UTC) - timedelta(days=30) for ref_id in referral_ids: ref_user = next((r for r in referrals_list if r.id == ref_id), None) @@ -334,7 +334,7 @@ class ReferralWithdrawalService: analysis['details']['earnings_by_reason'] = earnings_by_reason # 5. Проверка: много начислений за последнюю неделю - week_ago = datetime.utcnow() - timedelta(days=7) + week_ago = datetime.now(UTC) - timedelta(days=7) recent_earnings = await db.execute( select(func.count(), func.coalesce(func.sum(ReferralEarning.amount_kopeks), 0)).where( ReferralEarning.user_id == user_id, ReferralEarning.created_at >= week_ago @@ -480,14 +480,14 @@ class ReferralWithdrawalService: amount_kopeks=-request.amount_kopeks, description=f'Вывод реферального баланса (заявка #{request.id})', is_completed=True, - completed_at=datetime.utcnow(), + completed_at=datetime.now(UTC), ) db.add(withdrawal_tx) # Обновляем статус заявки request.status = WithdrawalRequestStatus.APPROVED.value request.processed_by = admin_id - request.processed_at = datetime.utcnow() + request.processed_at = datetime.now(UTC) request.admin_comment = comment await db.commit() @@ -505,7 +505,7 @@ class ReferralWithdrawalService: request.status = WithdrawalRequestStatus.REJECTED.value request.processed_by = admin_id - request.processed_at = datetime.utcnow() + request.processed_at = datetime.now(UTC) request.admin_comment = comment await db.commit() @@ -523,7 +523,7 @@ class ReferralWithdrawalService: request.status = WithdrawalRequestStatus.COMPLETED.value request.processed_by = admin_id - request.processed_at = datetime.utcnow() + request.processed_at = datetime.now(UTC) if comment: request.admin_comment = (request.admin_comment or '') + f'\n{comment}' diff --git a/app/services/remnawave_service.py b/app/services/remnawave_service.py index 460ac7f7..4677ba4c 100644 --- a/app/services/remnawave_service.py +++ b/app/services/remnawave_service.py @@ -2,7 +2,7 @@ import asyncio import re from contextlib import AsyncExitStack, asynccontextmanager from dataclasses import asdict, is_dataclass -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from typing import Any, Optional from zoneinfo import ZoneInfo @@ -204,14 +204,14 @@ class RemnaWaveService: getattr_2=getattr(user, 'telegram_id', '?'), ) mutation.set_user_uuid(conflicting_user, None) - mutation.set_user_updated_at(conflicting_user, datetime.utcnow()) + mutation.set_user_updated_at(conflicting_user, datetime.now(UTC)) mutation.remove_map_entry(panel_uuid) if current_uuid: mutation.remove_map_entry(current_uuid) mutation.set_user_uuid(user, panel_uuid) - mutation.set_user_updated_at(user, datetime.utcnow()) + mutation.set_user_updated_at(user, datetime.now(UTC)) mutation.set_map_entry(panel_uuid, user) logger.info( @@ -236,7 +236,7 @@ class RemnaWaveService: def _now_utc(self) -> datetime: """Возвращает текущее время в UTC без привязки к часовому поясу.""" - return datetime.now(self._utc_timezone).replace(tzinfo=None) + return datetime.now(self._utc_timezone) def _local_to_utc(self, local_dt: datetime) -> datetime: """Конвертирует naive локальную дату (в таймзоне панели/бота) в naive UTC. @@ -245,10 +245,10 @@ class RemnaWaveService: """ if local_dt.tzinfo is not None: # Уже есть tzinfo - конвертируем напрямую - return local_dt.astimezone(self._utc_timezone).replace(tzinfo=None) + return local_dt.astimezone(self._utc_timezone) # Naive datetime - интерпретируем как локальное время панели local_aware = local_dt.replace(tzinfo=self._panel_timezone) - return local_aware.astimezone(self._utc_timezone).replace(tzinfo=None) + return local_aware.astimezone(self._utc_timezone) def _parse_remnawave_date(self, date_str: str) -> datetime: if not date_str: @@ -270,9 +270,9 @@ class RemnaWaveService: # Панель RemnaWave всегда отдаёт время в UTC # Если есть tzinfo - конвертируем в UTC, иначе считаем что уже UTC if parsed_date.tzinfo is not None: - utc_normalized = parsed_date.astimezone(self._utc_timezone).replace(tzinfo=None) + utc_normalized = parsed_date.astimezone(self._utc_timezone) else: - utc_normalized = parsed_date + utc_normalized = parsed_date.replace(tzinfo=UTC) logger.debug('Успешно распарсена дата: (UTC)', date_str=date_str, utc_normalized=utc_normalized) return utc_normalized @@ -294,8 +294,6 @@ class RemnaWaveService: result = minimum_expire else: normalized_expire = expire_at - if normalized_expire.tzinfo is not None: - normalized_expire = normalized_expire.replace(tzinfo=None) if normalized_expire < minimum_expire: logger.debug( @@ -316,11 +314,11 @@ class RemnaWaveService: expire_at_value = panel_user.get('expireAt') if expire_at_value is None: - return datetime.min.replace(tzinfo=None) + return datetime.min.replace(tzinfo=UTC) expire_at_str = str(expire_at_value).strip() if not expire_at_str: - return datetime.min.replace(tzinfo=None) + return datetime.min.replace(tzinfo=UTC) return self._parse_remnawave_date(expire_at_str) @@ -681,7 +679,7 @@ class RemnaWaveService: }, 'nodes_realtime': realtime_usage, 'nodes_weekly': nodes_weekly_data, - 'last_updated': datetime.now(), + 'last_updated': datetime.now(UTC), } logger.info( @@ -1039,7 +1037,7 @@ class RemnaWaveService: continue subscription.connected_squads = new_squads - subscription.updated_at = datetime.utcnow() + subscription.updated_at = datetime.now(UTC) source_decrement += 1 if not had_target_before: @@ -1160,7 +1158,7 @@ class RemnaWaveService: 'status': user_obj.status.value, 'telegramId': user_obj.telegram_id, 'email': user_obj.email, # Email для синхронизации email-only пользователей - 'expireAt': user_obj.expire_at.replace(tzinfo=None).isoformat(), + 'expireAt': user_obj.expire_at.isoformat(), 'trafficLimitBytes': user_obj.traffic_limit_bytes, 'usedTrafficBytes': user_obj.used_traffic_bytes, 'hwidDeviceLimit': user_obj.hwid_device_limit, @@ -1550,7 +1548,7 @@ class RemnaWaveService: else: # Для триальных подписок - сбрасываем как раньше subscription.is_trial = True - subscription.end_date = datetime.utcnow() + subscription.end_date = datetime.now(UTC) subscription.traffic_limit_gb = 0 subscription.traffic_used_gb = 0.0 subscription.device_limit = 1 @@ -1566,7 +1564,7 @@ class RemnaWaveService: if old_uuid: cleanup_mutation.remove_map_entry(old_uuid) cleanup_mutation.set_user_uuid(db_user, None) - cleanup_mutation.set_user_updated_at(db_user, datetime.utcnow()) + cleanup_mutation.set_user_updated_at(db_user, datetime.now(UTC)) stats['deleted'] += 1 logger.info( @@ -1773,10 +1771,8 @@ class RemnaWaveService: if expire_at > local_end_date_utc: # RemnaWave имеет более позднюю дату - обновляем # Конвертируем UTC обратно в локальное время для сохранения в БД - new_end_date_local = ( - expire_at.replace(tzinfo=self._utc_timezone) - .astimezone(self._panel_timezone) - .replace(tzinfo=None) + new_end_date_local = expire_at.replace(tzinfo=self._utc_timezone).astimezone( + self._panel_timezone ) logger.info( '✅ Sync: обновлена end_date для user -> (разница: с)', @@ -1941,14 +1937,10 @@ class RemnaWaveService: expire_at = self._safe_expire_at_for_panel(sub.end_date) # Определяем статус для панели - is_subscription_active = ( - sub.status - in ( - SubscriptionStatus.ACTIVE.value, - SubscriptionStatus.TRIAL.value, - ) - and sub.end_date > datetime.utcnow() - ) + is_subscription_active = sub.status in ( + SubscriptionStatus.ACTIVE.value, + SubscriptionStatus.TRIAL.value, + ) and sub.end_date > datetime.now(UTC) status = UserStatus.ACTIVE if is_subscription_active else UserStatus.DISABLED username = settings.format_remnawave_username( @@ -2350,7 +2342,7 @@ class RemnaWaveService: node_realtime = stats break - end_date = datetime.now() + end_date = datetime.now(UTC) start_date = end_date - timedelta(days=7) usage_history = await self.get_node_user_usage_by_range(node_uuid, start_date, end_date) @@ -2359,7 +2351,7 @@ class RemnaWaveService: 'node': node, 'realtime': node_realtime, 'usage_history': usage_history, - 'last_updated': datetime.now().isoformat(), + 'last_updated': datetime.now(UTC).isoformat(), } except Exception as e: @@ -2886,7 +2878,7 @@ class RemnaWaveService: details = { 'api_url': settings.REMNAWAVE_API_URL, - 'last_check': datetime.utcnow(), + 'last_check': datetime.now(UTC), 'manual_message': message, } @@ -2944,7 +2936,7 @@ class RemnaWaveService: 'api_available': False, 'nodes_status': 'неизвестно', 'users_online': 0, - 'last_check': datetime.utcnow(), + 'last_check': datetime.now(UTC), 'has_issues': True, 'recommendation': 'Обратитесь к системному администратору', 'error': str(e), @@ -2959,7 +2951,7 @@ class RemnaWaveService: for attempt in range(1, attempts + 1): try: - start_time = datetime.utcnow() + start_time = datetime.now(UTC) async with self.get_api_client() as api: try: @@ -2981,7 +2973,7 @@ class RemnaWaveService: total_nodes = 0 nodes_health = 'unknown' - end_time = datetime.utcnow() + end_time = datetime.now(UTC) response_time = (end_time - start_time).total_seconds() if not api_available: @@ -3045,7 +3037,7 @@ class RemnaWaveService: 'nodes_online': 0, 'total_nodes': 0, 'nodes_health': 'unknown', - 'last_check': datetime.utcnow(), + 'last_check': datetime.now(UTC), 'api_url': settings.REMNAWAVE_API_URL, 'attempts_used': attempts, } diff --git a/app/services/remnawave_sync_service.py b/app/services/remnawave_sync_service.py index 1d111d84..6c5fd98a 100644 --- a/app/services/remnawave_sync_service.py +++ b/app/services/remnawave_sync_service.py @@ -1,7 +1,7 @@ import asyncio from collections.abc import Callable from dataclasses import dataclass -from datetime import datetime, time, timedelta +from datetime import UTC, datetime, time, timedelta from typing import Any import structlog @@ -128,7 +128,7 @@ class RemnaWaveAutoSyncService: return {'started': False, 'reason': 'already_running'} async with self._sync_lock: - self._last_run_started_at = datetime.utcnow() + self._last_run_started_at = datetime.now(UTC) self._last_run_finished_at = None self._last_run_reason = reason self._last_run_error = None @@ -142,7 +142,7 @@ class RemnaWaveAutoSyncService: self._last_run_success = False self._last_user_stats = None self._last_server_stats = None - self._last_run_finished_at = datetime.utcnow() + self._last_run_finished_at = datetime.now(UTC) logger.error('❌ Автосинхронизация RemnaWave', message=message) return { 'started': True, @@ -157,7 +157,7 @@ class RemnaWaveAutoSyncService: self._last_run_success = False self._last_user_stats = None self._last_server_stats = None - self._last_run_finished_at = datetime.utcnow() + self._last_run_finished_at = datetime.now(UTC) logger.exception('❌ Ошибка автосинхронизации RemnaWave', error=error) return { 'started': True, @@ -171,7 +171,7 @@ class RemnaWaveAutoSyncService: self._last_run_error = None self._last_user_stats = user_stats self._last_server_stats = server_stats - self._last_run_finished_at = datetime.utcnow() + self._last_run_finished_at = datetime.now(UTC) return { 'started': True, @@ -205,7 +205,7 @@ class RemnaWaveAutoSyncService: next_run = self._calculate_next_run(times) self._next_run = next_run - delay = (next_run - datetime.utcnow()).total_seconds() + delay = (next_run - datetime.now(UTC)).total_seconds() if delay > 0: await asyncio.sleep(delay) @@ -258,17 +258,17 @@ class RemnaWaveAutoSyncService: @staticmethod def _calculate_next_run(times: list[time]) -> datetime: - now = datetime.utcnow() + now = datetime.now(UTC) today = now.date() for scheduled in sorted(times): - candidate = datetime.combine(today, scheduled) + candidate = datetime.combine(today, scheduled, tzinfo=UTC) if candidate > now: return candidate first_time = sorted(times)[0] next_day = today + timedelta(days=1) - return datetime.combine(next_day, first_time) + return datetime.combine(next_day, first_time, tzinfo=UTC) def _create_service() -> RemnaWaveAutoSyncService: diff --git a/app/services/remnawave_webhook_service.py b/app/services/remnawave_webhook_service.py index 400defde..61d82895 100644 --- a/app/services/remnawave_webhook_service.py +++ b/app/services/remnawave_webhook_service.py @@ -457,7 +457,7 @@ class RemnaWaveWebhookService: @staticmethod def _stamp_webhook_update(subscription: Subscription) -> None: """Mark subscription as recently updated by webhook to prevent sync overwrite.""" - subscription.last_webhook_update_at = datetime.now(UTC).replace(tzinfo=None) + subscription.last_webhook_update_at = datetime.now(UTC) # ------------------------------------------------------------------ # User event handlers @@ -569,7 +569,7 @@ class RemnaWaveWebhookService: if expire_at: try: parsed_dt = datetime.fromisoformat(expire_at.replace('Z', '+00:00')) - new_end_date = parsed_dt.astimezone(UTC).replace(tzinfo=None) + new_end_date = parsed_dt.astimezone(UTC) if subscription.end_date != new_end_date: subscription.end_date = new_end_date changed = True @@ -579,7 +579,7 @@ class RemnaWaveWebhookService: # Sync status from panel panel_status = data.get('status') if panel_status: - now = datetime.now(UTC).replace(tzinfo=None) + now = datetime.now(UTC) end_date = subscription.end_date if panel_status == 'ACTIVE' and end_date and end_date > now: if subscription.status != SubscriptionStatus.ACTIVE.value: @@ -609,7 +609,7 @@ class RemnaWaveWebhookService: # Always stamp to protect from sync overwrite, even if no fields changed self._stamp_webhook_update(subscription) if changed: - subscription.updated_at = datetime.now(UTC).replace(tzinfo=None) + subscription.updated_at = datetime.now(UTC) logger.info( 'Webhook: subscription modified (synced from panel) for user', subscription_id=subscription.id, @@ -668,7 +668,7 @@ class RemnaWaveWebhookService: subscription.subscription_crypto_link = None subscription.remnawave_short_uuid = None subscription.connected_squads = None - subscription.updated_at = datetime.now(UTC).replace(tzinfo=None) + subscription.updated_at = datetime.now(UTC) # Remove SubscriptionServer link rows await db.execute(delete(SubscriptionServer).where(SubscriptionServer.subscription_id == sub_id)) @@ -703,7 +703,7 @@ class RemnaWaveWebhookService: # Always stamp to protect from sync overwrite self._stamp_webhook_update(subscription) if changed: - subscription.updated_at = datetime.now(UTC).replace(tzinfo=None) + subscription.updated_at = datetime.now(UTC) logger.info( 'Webhook: subscription credentials revoked/updated for user', subscription_id=subscription.id, diff --git a/app/services/reporting_service.py b/app/services/reporting_service.py index 748c7aed..b7995295 100644 --- a/app/services/reporting_service.py +++ b/app/services/reporting_service.py @@ -232,8 +232,8 @@ class ReportingService: report_date: date | None, ) -> str: period_range = self._get_period_range(period, report_date) - start_utc = period_range.start_msk.astimezone(UTC).replace(tzinfo=None) - end_utc = period_range.end_msk.astimezone(UTC).replace(tzinfo=None) + start_utc = period_range.start_msk.astimezone(UTC) + end_utc = period_range.end_msk.astimezone(UTC) async with AsyncSessionLocal() as session: totals = await self._collect_current_totals(session) @@ -523,7 +523,7 @@ class ReportingService: ] async def _get_user_usage_stats(self, session) -> dict[str, int]: - now_utc = datetime.now(UTC).replace(tzinfo=None) + now_utc = datetime.now(UTC) active_paid_q = await session.execute( select(func.count(func.distinct(Subscription.user_id))).where( diff --git a/app/services/startup_notification_service.py b/app/services/startup_notification_service.py index 1d8b77d4..8821aa3c 100644 --- a/app/services/startup_notification_service.py +++ b/app/services/startup_notification_service.py @@ -4,7 +4,7 @@ Отправляет красивое сообщение с информацией о системе при запуске бота. """ -from datetime import datetime +from datetime import UTC, datetime from typing import Final import structlog @@ -228,7 +228,7 @@ class StartupNotificationService: ] system_info = '\n'.join(system_info_lines) - timestamp = format_local_datetime(datetime.utcnow(), DATETIME_FORMAT) + timestamp = format_local_datetime(datetime.now(UTC), DATETIME_FORMAT) message = ( f'Remnawave Bedolaga Bot\n\n' @@ -388,7 +388,7 @@ async def send_crash_notification(bot: Bot, error: Exception, traceback_str: str return False try: - timestamp = format_local_datetime(datetime.utcnow(), DATETIME_FORMAT) + timestamp = format_local_datetime(datetime.now(UTC), DATETIME_FORMAT) error_type = type(error).__name__ error_message = str(error)[:CRASH_ERROR_MESSAGE_MAX_LENGTH] separator = '=' * REPORT_SEPARATOR_WIDTH @@ -407,7 +407,7 @@ async def send_crash_notification(bot: Bot, error: Exception, traceback_str: str ) # Создаем файл для отправки - file_name = f'crash_report_{datetime.utcnow().strftime(DATETIME_FORMAT_FILENAME)}.txt' + file_name = f'crash_report_{datetime.now(UTC).strftime(DATETIME_FORMAT_FILENAME)}.txt' file = BufferedInputFile( file=log_content.encode('utf-8'), filename=file_name, diff --git a/app/services/subscription_auto_purchase_service.py b/app/services/subscription_auto_purchase_service.py index ef10ed44..14e08e01 100644 --- a/app/services/subscription_auto_purchase_service.py +++ b/app/services/subscription_auto_purchase_service.py @@ -861,7 +861,7 @@ async def _auto_purchase_daily_tariff( bot: Bot | None = None, ) -> bool: """Автоматическая покупка суточного тарифа из сохранённой корзины.""" - from datetime import datetime, timedelta + from datetime import UTC, datetime, timedelta # Lazy imports to avoid circular dependency from app.cabinet.routes.websocket import ( @@ -957,9 +957,9 @@ async def _auto_purchase_daily_tariff( existing_subscription.connected_squads = squads existing_subscription.status = 'active' existing_subscription.is_trial = False - existing_subscription.last_daily_charge_at = datetime.utcnow() + existing_subscription.last_daily_charge_at = datetime.now(UTC) existing_subscription.is_daily_paused = False - existing_subscription.end_date = datetime.utcnow() + timedelta(days=1) + existing_subscription.end_date = datetime.now(UTC) + timedelta(days=1) if was_trial_conversion: user.has_had_paid_subscription = True await db.commit() @@ -978,7 +978,7 @@ async def _auto_purchase_daily_tariff( tariff_id=tariff.id, ) # Устанавливаем параметры для суточного списания - subscription.last_daily_charge_at = datetime.utcnow() + subscription.last_daily_charge_at = datetime.now(UTC) subscription.is_daily_paused = False await db.commit() was_trial_conversion = False @@ -1526,7 +1526,7 @@ async def auto_purchase_saved_cart_after_topup( bot: Bot | None = None, ) -> bool: """Attempts to automatically purchase a subscription from a saved cart.""" - from datetime import datetime, timedelta + from datetime import UTC, datetime, timedelta # Lazy imports to avoid circular dependency from app.cabinet.routes.websocket import ( @@ -1559,12 +1559,12 @@ async def auto_purchase_saved_cart_after_topup( if ( last_tx.type == TransactionType.SUBSCRIPTION_PAYMENT and last_tx.created_at - and (datetime.utcnow() - last_tx.created_at) < timedelta(seconds=60) + and (datetime.now(UTC) - last_tx.created_at) < timedelta(seconds=60) ): logger.info( '🔁 Автопокупка: пропускаем для пользователя - подписка уже куплена секунд назад', format_user_id=_format_user_id(user), - total_seconds=(datetime.utcnow() - last_tx.created_at).total_seconds(), + total_seconds=(datetime.now(UTC) - last_tx.created_at).total_seconds(), ) # Очищаем корзину чтобы не срабатывало повторно await user_cart_service.delete_user_cart(user.id) diff --git a/app/services/subscription_purchase_service.py b/app/services/subscription_purchase_service.py index d7c3b395..e74968da 100644 --- a/app/services/subscription_purchase_service.py +++ b/app/services/subscription_purchase_service.py @@ -1,6 +1,6 @@ from collections.abc import Sequence from dataclasses import dataclass, field -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from typing import Any import structlog @@ -1064,7 +1064,7 @@ class MiniAppSubscriptionPurchaseService: context.subscription = subscription was_trial_conversion = False - now = datetime.utcnow() + now = datetime.now(UTC) if subscription: bonus_period = timedelta() diff --git a/app/services/subscription_service.py b/app/services/subscription_service.py index 95674be1..4db297b5 100644 --- a/app/services/subscription_service.py +++ b/app/services/subscription_service.py @@ -1,5 +1,5 @@ from contextlib import asynccontextmanager -from datetime import datetime +from datetime import UTC, datetime import structlog from sqlalchemy.ext.asyncio import AsyncSession @@ -357,7 +357,7 @@ class SubscriptionService: except Exception: pass # tariff может быть None или уже загружен - current_time = datetime.utcnow() + current_time = datetime.now(UTC) # Определяем актуальный статус для отправки в RemnaWave # НЕ меняем статус подписки здесь - это задача scheduled job is_actually_active = ( diff --git a/app/services/traffic_monitoring_service.py b/app/services/traffic_monitoring_service.py index b760b4e7..c987f25f 100644 --- a/app/services/traffic_monitoring_service.py +++ b/app/services/traffic_monitoring_service.py @@ -5,7 +5,7 @@ import asyncio from dataclasses import dataclass -from datetime import datetime, time, timedelta +from datetime import UTC, datetime, time, timedelta import structlog from sqlalchemy.ext.asyncio import AsyncSession @@ -121,7 +121,7 @@ class TrafficMonitoringServiceV2: success = await cache.set(TRAFFIC_SNAPSHOT_KEY, snapshot_data, expire=ttl) if success: # Сохраняем время создания snapshot - await cache.set(TRAFFIC_SNAPSHOT_TIME_KEY, datetime.utcnow().isoformat(), expire=ttl) + await cache.set(TRAFFIC_SNAPSHOT_TIME_KEY, datetime.now(UTC).isoformat(), expire=ttl) logger.info( '📦 Snapshot сохранён в Redis: пользователей, TTL ч', snapshot_count=len(snapshot), @@ -165,7 +165,7 @@ class TrafficMonitoringServiceV2: try: key = cache_key(TRAFFIC_NOTIFICATION_CACHE_KEY, user_uuid) ttl = 24 * 3600 # 24 часа - return await cache.set(key, datetime.utcnow().isoformat(), expire=ttl) + return await cache.set(key, datetime.now(UTC).isoformat(), expire=ttl) except Exception as e: logger.error('❌ Ошибка сохранения уведомления в Redis', error=e) return False @@ -235,7 +235,7 @@ class TrafficMonitoringServiceV2: return True cooldown = self.get_notification_cooldown_seconds() - return (datetime.utcnow() - last_notification).total_seconds() > cooldown + return (datetime.now(UTC) - last_notification).total_seconds() > cooldown async def record_notification(self, user_uuid: str): """Записывает время отправки уведомления (Redis + fallback на память)""" @@ -244,11 +244,11 @@ class TrafficMonitoringServiceV2: # Fallback на память if not saved: - self._memory_notification_cache[user_uuid] = datetime.utcnow() + self._memory_notification_cache[user_uuid] = datetime.now(UTC) async def cleanup_notification_cache(self): """Очищает старые записи из памяти (Redis очищается автоматически через TTL)""" - now = datetime.utcnow() + now = datetime.now(UTC) expired = [uuid for uuid, dt in self._memory_notification_cache.items() if (now - dt) > timedelta(hours=24)] for uuid in expired: del self._memory_notification_cache[uuid] @@ -313,7 +313,7 @@ class TrafficMonitoringServiceV2: if not snapshot_time: return float('inf') - return (datetime.utcnow() - snapshot_time).total_seconds() / 60 + return (datetime.now(UTC) - snapshot_time).total_seconds() / 60 async def _get_current_snapshot(self) -> dict[str, float]: """Получает текущий snapshot (Redis + fallback на память)""" @@ -338,7 +338,7 @@ class TrafficMonitoringServiceV2: # Fallback на память self._memory_snapshot = snapshot.copy() - self._memory_snapshot_time = datetime.utcnow() + self._memory_snapshot_time = datetime.now(UTC) logger.warning('⚠️ Redis недоступен, snapshot сохранён в память') return True @@ -360,7 +360,7 @@ class TrafficMonitoringServiceV2: return len(existing_snapshot) logger.info('📸 Создание начального snapshot трафика...') - start_time = datetime.utcnow() + start_time = datetime.now(UTC) users = await self.get_all_users_with_traffic() new_snapshot: dict[str, float] = {} @@ -383,7 +383,7 @@ class TrafficMonitoringServiceV2: # Сохраняем в Redis (с fallback на память) await self._save_snapshot(new_snapshot) - elapsed = (datetime.utcnow() - start_time).total_seconds() + elapsed = (datetime.now(UTC) - start_time).total_seconds() logger.info( '✅ Snapshot создан за с: пользователей', elapsed=round(elapsed, 1), new_snapshot_count=len(new_snapshot) ) @@ -402,7 +402,7 @@ class TrafficMonitoringServiceV2: if not self.is_fast_check_enabled(): return [] - start_time = datetime.utcnow() + start_time = datetime.now(UTC) is_first_run = not await self.has_snapshot() # Загружаем кеш нод для красивых названий в уведомлениях @@ -535,7 +535,7 @@ class TrafficMonitoringServiceV2: await self._save_snapshot(new_snapshot) logger.info('💾 Новый snapshot сохранён: пользователей', new_snapshot_count=len(new_snapshot)) - elapsed = (datetime.utcnow() - start_time).total_seconds() + elapsed = (datetime.now(UTC) - start_time).total_seconds() if is_first_run: logger.info( @@ -567,7 +567,7 @@ class TrafficMonitoringServiceV2: return [] logger.info('🚀 Запуск суточной проверки трафика...') - start_time = datetime.utcnow() + start_time = datetime.now(UTC) # Загружаем кеш нод для красивых названий в уведомлениях await self._load_nodes_cache() @@ -576,7 +576,7 @@ class TrafficMonitoringServiceV2: threshold_bytes = self.get_daily_threshold_gb() * (1024**3) # Получаем период за последние 24 часа - now = datetime.utcnow() + now = datetime.now(UTC) start_date = (now - timedelta(hours=24)).strftime('%Y-%m-%d') end_date = now.strftime('%Y-%m-%d') @@ -639,7 +639,7 @@ class TrafficMonitoringServiceV2: if isinstance(result, TrafficViolation): violations.append(result) - elapsed = (datetime.utcnow() - start_time).total_seconds() + elapsed = (datetime.now(UTC) - start_time).total_seconds() logger.info( '✅ Суточная проверка завершена за с: пользователей, превышений', elapsed=round(elapsed, 1), @@ -725,7 +725,7 @@ class TrafficMonitoringServiceV2: elif violation.last_node_uuid: message += f'\n🖥 Сервер: {violation.last_node_uuid}' - message += f'\n\n⏰ {datetime.utcnow().strftime("%d.%m.%Y %H:%M:%S")} UTC' + message += f'\n\n⏰ {datetime.now(UTC).strftime("%d.%m.%Y %H:%M:%S")} UTC' await admin_service.send_suspicious_traffic_notification(message, bot, topic_id) await self.record_notification(violation.user_uuid) @@ -831,8 +831,8 @@ class TrafficMonitoringSchedulerV2: while self._is_running: try: # Вычисляем время до следующей проверки - now = datetime.utcnow() - next_run = datetime.combine(now.date(), check_time) + now = datetime.now(UTC) + next_run = datetime.combine(now.date(), check_time, tzinfo=UTC) if next_run <= now: next_run += timedelta(days=1) diff --git a/app/services/tribute_service.py b/app/services/tribute_service.py index 38e9bc27..5978a2e1 100644 --- a/app/services/tribute_service.py +++ b/app/services/tribute_service.py @@ -1,5 +1,5 @@ import json -from datetime import datetime +from datetime import UTC, datetime from typing import Any import structlog @@ -148,7 +148,7 @@ class TributeService: was_first_topup = not user.has_made_first_topup user.balance_kopeks += amount_kopeks - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) promo_group = user.get_primary_promo_group() subscription = getattr(user, 'subscription', None) @@ -440,7 +440,7 @@ class TributeService: logger.error('⌘ Пользователь не найден', user_id=user_id) return False - external_id = f'force_donation_{payment_id}_{int(datetime.utcnow().timestamp())}' + external_id = f'force_donation_{payment_id}_{int(datetime.now(UTC).timestamp())}' await create_transaction( db=session, @@ -455,7 +455,7 @@ class TributeService: old_balance = user.balance_kopeks user.balance_kopeks += amount_kopeks - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) await session.commit() diff --git a/app/services/user_service.py b/app/services/user_service.py index ac0f923d..97a9b2d9 100644 --- a/app/services/user_service.py +++ b/app/services/user_service.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from typing import Any import structlog @@ -209,7 +209,7 @@ class UserService: 'subscription': subscription, 'transactions_count': transactions_count, 'is_admin': settings.is_admin(user.telegram_id, user.email), - 'registration_days': (datetime.utcnow() - user.created_at).days, + 'registration_days': (datetime.now(UTC) - user.created_at).days, } except Exception as e: @@ -305,7 +305,7 @@ class UserService: """Возвращает пользователей с истекшей подпиской и достаточным балансом.""" try: offset = (page - 1) * limit - now = datetime.utcnow() + now = datetime.now(UTC) base_filters = [ User.balance_kopeks >= min_balance_kopeks, @@ -584,7 +584,7 @@ class UserService: user.promo_group_id = promo_group.id user.promo_group = promo_group - user.updated_at = datetime.utcnow() + user.updated_at = datetime.now(UTC) await db.commit() await db.refresh(user) @@ -699,11 +699,11 @@ class UserService: await update_user(db, user, status=UserStatus.ACTIVE.value) if user.subscription: - from datetime import datetime + from datetime import UTC, datetime from app.database.models import SubscriptionStatus - if user.subscription.end_date > datetime.utcnow(): + if user.subscription.end_date > datetime.now(UTC): user.subscription.status = SubscriptionStatus.ACTIVE.value await db.commit() await db.refresh(user.subscription) @@ -1228,9 +1228,9 @@ class UserService: subscription = await get_subscription_by_user_id(db, user_id) transactions_count = await get_user_transactions_count(db, user_id) - days_since_registration = (datetime.utcnow() - user.created_at).days + days_since_registration = (datetime.now(UTC) - user.created_at).days - days_since_activity = (datetime.utcnow() - user.last_activity).days if user.last_activity else None + days_since_activity = (datetime.now(UTC) - user.last_activity).days if user.last_activity else None return { 'user_id': user.id, @@ -1283,7 +1283,7 @@ class UserService: continue if days_inactive and user.last_activity: - inactive_threshold = datetime.utcnow() - timedelta(days=days_inactive) + inactive_threshold = datetime.now(UTC) - timedelta(days=days_inactive) if user.last_activity > inactive_threshold: continue diff --git a/app/services/version_service.py b/app/services/version_service.py index c36ed6cd..7995a892 100644 --- a/app/services/version_service.py +++ b/app/services/version_service.py @@ -1,6 +1,6 @@ import asyncio import re -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta import aiohttp import structlog @@ -132,7 +132,7 @@ class VersionService: async def _fetch_releases(self, force: bool = False) -> list[VersionInfo]: if not force and self._cache and self._last_check: - if datetime.now() - self._last_check < timedelta(seconds=self.cache_ttl): + if datetime.now(UTC) - self._last_check < timedelta(seconds=self.cache_ttl): return self._cache.get('releases', []) url = f'https://api.github.com/repos/{self.repo}/releases' @@ -155,7 +155,7 @@ class VersionService: releases.append(release) self._cache['releases'] = releases - self._last_check = datetime.now() + self._last_check = datetime.now(UTC) logger.info('Получено релизов из GitHub', releases_count=len(releases)) return releases diff --git a/app/services/wata_service.py b/app/services/wata_service.py index ecd2825b..be1e60a2 100644 --- a/app/services/wata_service.py +++ b/app/services/wata_service.py @@ -153,8 +153,8 @@ class WataService: normalized = raw.replace('Z', '+00:00') parsed = datetime.fromisoformat(normalized) if parsed.tzinfo is None: - return parsed - return parsed.astimezone(UTC).replace(tzinfo=None) + return parsed.replace(tzinfo=UTC) + return parsed.astimezone(UTC) except (ValueError, TypeError): logger.debug('Failed to parse WATA datetime', raw=raw) return None @@ -192,7 +192,7 @@ class WataService: expiration_minutes = int(ttl) if ttl is not None else None if expiration_minutes: - expiration_time = datetime.utcnow() + timedelta(minutes=expiration_minutes) + expiration_time = datetime.now(UTC) + timedelta(minutes=expiration_minutes) payload['expirationDateTime'] = self._format_datetime(expiration_time) if allow_arbitrary_amount: diff --git a/app/services/web_api_token_service.py b/app/services/web_api_token_service.py index b46dc943..92c7349d 100644 --- a/app/services/web_api_token_service.py +++ b/app/services/web_api_token_service.py @@ -1,7 +1,7 @@ from __future__ import annotations import secrets -from datetime import datetime +from datetime import UTC, datetime from sqlalchemy.ext.asyncio import AsyncSession @@ -47,10 +47,10 @@ class WebApiTokenService: if not token or not token.is_active: return None - if token.expires_at and token.expires_at < datetime.utcnow(): + if token.expires_at and token.expires_at < datetime.now(UTC): return None - token.last_used_at = datetime.utcnow() + token.last_used_at = datetime.now(UTC) if remote_ip: token.last_used_ip = remote_ip await db.flush() @@ -83,14 +83,14 @@ class WebApiTokenService: async def revoke_token(self, db: AsyncSession, token: WebApiToken) -> WebApiToken: token.is_active = False - token.updated_at = datetime.utcnow() + token.updated_at = datetime.now(UTC) await db.flush() await db.refresh(token) return token async def activate_token(self, db: AsyncSession, token: WebApiToken) -> WebApiToken: token.is_active = True - token.updated_at = datetime.utcnow() + token.updated_at = datetime.now(UTC) await db.flush() await db.refresh(token) return token diff --git a/app/services/wheel_service.py b/app/services/wheel_service.py index 0236d06c..24414c10 100644 --- a/app/services/wheel_service.py +++ b/app/services/wheel_service.py @@ -5,7 +5,7 @@ import random import secrets from dataclasses import dataclass -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from decimal import Decimal from typing import Any @@ -305,7 +305,7 @@ class FortuneWheelService: # Уменьшаем end_date subscription.end_date -= timedelta(days=config.spin_cost_days) - subscription.updated_at = datetime.utcnow() + subscription.updated_at = datetime.now(UTC) # Оцениваем стоимость в копейках (для статистики) # Берем цену 30-дневного периода и делим на 30 @@ -395,7 +395,7 @@ class FortuneWheelService: else: # Обычная подписка - добавляем дни и синхронизируем с RemnaWave subscription.end_date += timedelta(days=prize.prize_value) - subscription.updated_at = datetime.utcnow() + subscription.updated_at = datetime.now(UTC) logger.info('📅 Начислено дней подписки user_id', prize_value=prize.prize_value, user_id=user.id) # Синхронизируем с RemnaWave @@ -422,7 +422,7 @@ class FortuneWheelService: subscription = await get_subscription_by_user_id(db, user.id) if subscription and subscription.traffic_limit_gb > 0: subscription.traffic_limit_gb += prize.prize_value - subscription.updated_at = datetime.utcnow() + subscription.updated_at = datetime.now(UTC) logger.info('📊 Начислено трафика user_id', prize_value=prize.prize_value, user_id=user.id) # Синхронизируем с RemnaWave @@ -470,7 +470,7 @@ class FortuneWheelService: balance_bonus_kopeks=prize.promo_balance_bonus_kopeks, subscription_days=prize.promo_subscription_days, max_uses=1, - valid_until=datetime.utcnow() + timedelta(days=config.promo_validity_days), + valid_until=datetime.now(UTC) + timedelta(days=config.promo_validity_days), is_active=True, created_by=user.id, ) diff --git a/app/utils/currency_converter.py b/app/utils/currency_converter.py index 6ee10738..49807a83 100644 --- a/app/utils/currency_converter.py +++ b/app/utils/currency_converter.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import UTC, datetime import aiohttp import structlog @@ -17,7 +17,7 @@ class CurrencyConverter: """Получает курс USD/RUB с кешированием""" cache_key = 'USD_RUB' - now = datetime.utcnow() + now = datetime.now(UTC) # Проверяем кеш if ( diff --git a/app/utils/formatters.py b/app/utils/formatters.py index 664b1c42..07af37f1 100644 --- a/app/utils/formatters.py +++ b/app/utils/formatters.py @@ -1,15 +1,15 @@ -from datetime import datetime +from datetime import UTC, datetime def format_datetime(dt: datetime | str, format_str: str = '%d.%m.%Y %H:%M') -> str: if isinstance(dt, str): if dt == 'now' or dt == '': - dt = datetime.now() + dt = datetime.now(UTC) else: try: dt = datetime.fromisoformat(dt.replace('Z', '+00:00')) except (ValueError, AttributeError): - dt = datetime.now() + dt = datetime.now(UTC) return dt.strftime(format_str) @@ -17,12 +17,12 @@ def format_datetime(dt: datetime | str, format_str: str = '%d.%m.%Y %H:%M') -> s def format_date(dt: datetime | str, format_str: str = '%d.%m.%Y') -> str: if isinstance(dt, str): if dt == 'now' or dt == '': - dt = datetime.now() + dt = datetime.now(UTC) else: try: dt = datetime.fromisoformat(dt.replace('Z', '+00:00')) except (ValueError, AttributeError): - dt = datetime.now() + dt = datetime.now(UTC) return dt.strftime(format_str) @@ -30,14 +30,14 @@ def format_date(dt: datetime | str, format_str: str = '%d.%m.%Y') -> str: def format_time_ago(dt: datetime | str, language: str = 'ru') -> str: if isinstance(dt, str): if dt == 'now' or dt == '': - dt = datetime.now() + dt = datetime.now(UTC) else: try: dt = datetime.fromisoformat(dt.replace('Z', '+00:00')) except (ValueError, AttributeError): - dt = datetime.now() + dt = datetime.now(UTC) - now = datetime.utcnow() + now = datetime.now(UTC) diff = now - dt language_code = (language or 'ru').split('-')[0].lower() @@ -179,7 +179,7 @@ def format_subscription_status(is_active: bool, is_trial: bool, end_date: dateti try: end_date = datetime.fromisoformat(end_date.replace('Z', '+00:00')) except (ValueError, AttributeError): - end_date = datetime.now() + end_date = datetime.now(UTC) language_code = (language or 'ru').split('-')[0].lower() use_russian_fallback = language_code in {'ru', 'fa'} @@ -192,7 +192,7 @@ def format_subscription_status(is_active: bool, is_trial: bool, end_date: dateti else: status = '✅ Активна' if use_russian_fallback else '✅ Active' - now = datetime.utcnow() + now = datetime.now(UTC) if end_date > now: days_left = (end_date - now).days if days_left > 0: diff --git a/app/utils/pricing_utils.py b/app/utils/pricing_utils.py index eb0ffb76..8818ad2b 100644 --- a/app/utils/pricing_utils.py +++ b/app/utils/pricing_utils.py @@ -1,5 +1,5 @@ from collections.abc import Sequence -from datetime import datetime +from datetime import UTC, datetime from typing import TYPE_CHECKING, Any, Optional import structlog @@ -19,7 +19,7 @@ def calculate_months_from_days(days: int) -> int: def get_remaining_months(end_date: datetime) -> int: - current_time = datetime.utcnow() + current_time = datetime.now(UTC) if end_date <= current_time: return 1 diff --git a/app/utils/promo_offer.py b/app/utils/promo_offer.py index 81fac66d..ada2292b 100644 --- a/app/utils/promo_offer.py +++ b/app/utils/promo_offer.py @@ -3,7 +3,7 @@ from __future__ import annotations import html import math from collections.abc import Sequence -from datetime import datetime +from datetime import UTC, datetime from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession @@ -29,7 +29,7 @@ def get_user_active_promo_discount_percent(user: User | None) -> int: return 0 expires_at = getattr(user, 'promo_offer_discount_expires_at', None) - if expires_at and expires_at <= datetime.utcnow(): + if expires_at and expires_at <= datetime.now(UTC): return 0 return max(0, min(100, percent)) @@ -78,7 +78,7 @@ async def build_promo_offer_timer_line( if not expires_at: return None - now = datetime.utcnow() + now = datetime.now(UTC) if expires_at <= now: return None @@ -161,7 +161,7 @@ async def build_test_access_hint( if not subscription_id: return None - now = datetime.utcnow() + now = datetime.now(UTC) result = await db.execute( select(SubscriptionTemporaryAccess) diff --git a/app/utils/subscription_utils.py b/app/utils/subscription_utils.py index 1dcbcd62..a54a335f 100644 --- a/app/utils/subscription_utils.py +++ b/app/utils/subscription_utils.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import UTC, datetime from urllib.parse import quote, urlparse, urlunparse import structlog @@ -54,7 +54,7 @@ async def update_or_create_subscription(db: AsyncSession, user_id: int, **subscr if hasattr(existing_subscription, key): setattr(existing_subscription, key, value) - existing_subscription.updated_at = datetime.utcnow() + existing_subscription.updated_at = datetime.now(UTC) await db.commit() await db.refresh(existing_subscription) diff --git a/app/utils/timezone.py b/app/utils/timezone.py index 26d1ddef..576f286e 100644 --- a/app/utils/timezone.py +++ b/app/utils/timezone.py @@ -31,16 +31,16 @@ def get_local_timezone() -> ZoneInfo: return ZoneInfo('UTC') -def panel_datetime_to_naive_utc(dt: datetime) -> datetime: - """Convert a panel datetime to naive UTC. +def panel_datetime_to_utc(dt: datetime) -> datetime: + """Convert a panel datetime to aware UTC. Panel API returns local time with a misleading UTC offset (+00:00 / Z). This strips the offset, interprets the raw value as panel-local time, - then converts to naive UTC for database storage. + then converts to aware UTC for database storage. """ - naive = dt.replace(tzinfo=None) + naive = dt localized = naive.replace(tzinfo=get_local_timezone()) - return localized.astimezone(ZoneInfo('UTC')).replace(tzinfo=None) + return localized.astimezone(ZoneInfo('UTC')) def to_local_datetime(dt: datetime | None) -> datetime | None: diff --git a/app/utils/user_utils.py b/app/utils/user_utils.py index def4ad2d..deef5c15 100644 --- a/app/utils/user_utils.py +++ b/app/utils/user_utils.py @@ -1,6 +1,6 @@ import secrets import string -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta import structlog from sqlalchemy import and_, func, select, update @@ -54,7 +54,7 @@ async def generate_unique_referral_code(db: AsyncSession, telegram_id: int) -> s if not result.scalar_one_or_none(): return code - timestamp = str(int(datetime.utcnow().timestamp()))[-6:] + timestamp = str(int(datetime.now(UTC).timestamp()))[-6:] return f'ref{timestamp}' @@ -85,7 +85,7 @@ async def mark_user_as_had_paid_subscription(db: AsyncSession, user: User) -> bo return True await db.execute( - update(User).where(User.id == user.id).values(has_had_paid_subscription=True, updated_at=datetime.utcnow()) + update(User).where(User.id == user.id).values(has_had_paid_subscription=True, updated_at=datetime.now(UTC)) ) await db.commit() @@ -116,7 +116,7 @@ async def get_user_referral_summary(db: AsyncSession, user_id: int) -> dict: ) total_earned_kopeks = total_earnings_result.scalar() or 0 - month_ago = datetime.utcnow() - timedelta(days=30) + month_ago = datetime.now(UTC) - timedelta(days=30) month_earnings_result = await db.execute( select(func.coalesce(func.sum(ReferralEarning.amount_kopeks), 0)).where( and_(ReferralEarning.user_id == user_id, ReferralEarning.created_at >= month_ago) @@ -223,11 +223,11 @@ async def get_detailed_referral_list(db: AsyncSession, user_id: int, limit: int ) topups_count = topups_result.scalar() or 0 - days_since_registration = (datetime.utcnow() - referral.created_at).days + days_since_registration = (datetime.now(UTC) - referral.created_at).days days_since_activity = None if referral.last_activity: - days_since_activity = (datetime.utcnow() - referral.last_activity).days + days_since_activity = (datetime.now(UTC) - referral.last_activity).days detailed_referrals.append( { @@ -270,7 +270,7 @@ async def get_detailed_referral_list(db: AsyncSession, user_id: int, limit: int async def get_referral_analytics(db: AsyncSession, user_id: int) -> dict: try: - now = datetime.utcnow() + now = datetime.now(UTC) periods = { 'today': now.replace(hour=0, minute=0, second=0, microsecond=0), 'week': now - timedelta(days=7), diff --git a/app/webapi/background/backup_tasks.py b/app/webapi/background/backup_tasks.py index fd05f8e2..72ed19c0 100644 --- a/app/webapi/background/backup_tasks.py +++ b/app/webapi/background/backup_tasks.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio import uuid from dataclasses import dataclass, field -from datetime import datetime +from datetime import UTC, datetime from app.services.backup_service import backup_service @@ -15,8 +15,8 @@ class BackupTaskState: message: str | None = None file_path: str | None = None created_by: int | None = None - created_at: datetime = field(default_factory=datetime.utcnow) - updated_at: datetime = field(default_factory=datetime.utcnow) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) + updated_at: datetime = field(default_factory=lambda: datetime.now(UTC)) class BackupTaskManager: @@ -36,7 +36,7 @@ class BackupTaskManager: async def _run_task(self, state: BackupTaskState) -> None: state.status = 'running' - state.updated_at = datetime.utcnow() + state.updated_at = datetime.now(UTC) try: success, message, file_path = await backup_service.create_backup(created_by=state.created_by) @@ -47,7 +47,7 @@ class BackupTaskManager: state.status = 'failed' state.message = f'Unexpected error: {exc}' finally: - state.updated_at = datetime.utcnow() + state.updated_at = datetime.now(UTC) async def get(self, task_id: str) -> BackupTaskState | None: async with self._lock: diff --git a/app/webapi/routes/broadcasts.py b/app/webapi/routes/broadcasts.py index 60e21a0e..2d63fe54 100644 --- a/app/webapi/routes/broadcasts.py +++ b/app/webapi/routes/broadcasts.py @@ -1,6 +1,6 @@ from __future__ import annotations -from datetime import datetime +from datetime import UTC, datetime from typing import Any from fastapi import APIRouter, Depends, HTTPException, Query, status @@ -138,7 +138,7 @@ async def stop_broadcast( broadcast.status = 'cancelling' else: broadcast.status = 'cancelled' - broadcast.completed_at = datetime.utcnow() + broadcast.completed_at = datetime.now(UTC) await db.commit() await db.refresh(broadcast) diff --git a/app/webapi/routes/contests.py b/app/webapi/routes/contests.py index 5b08c3fd..54323bd1 100644 --- a/app/webapi/routes/contests.py +++ b/app/webapi/routes/contests.py @@ -73,7 +73,7 @@ def _to_utc_naive(dt: datetime, tz_name: str | None = None) -> datetime: tz = ZoneInfo(tz_name or settings.TIMEZONE) if dt.tzinfo is None: dt = dt.replace(tzinfo=tz) - return dt.astimezone(UTC).replace(tzinfo=None) + return dt.astimezone(UTC) def _serialize_template(tpl: ContestTemplate) -> ContestTemplateResponse: @@ -301,7 +301,7 @@ async def start_round_now( if existing and payload.force: await finish_round(db, existing) - starts_at = payload.starts_at or datetime.utcnow() + starts_at = payload.starts_at or datetime.now(UTC) ends_at = payload.ends_at if payload.cooldown_hours and not ends_at: ends_at = starts_at + timedelta(hours=payload.cooldown_hours) @@ -619,7 +619,7 @@ async def delete_referral( contest = await get_referral_contest(db, contest_id) if not contest: raise HTTPException(status.HTTP_404_NOT_FOUND, 'Contest not found') - now_utc = datetime.utcnow() + now_utc = datetime.now(UTC) if contest.is_active or contest.end_at > now_utc: raise HTTPException( status.HTTP_400_BAD_REQUEST, diff --git a/app/webapi/routes/menu_layout.py b/app/webapi/routes/menu_layout.py index c7e8fe08..8a11f619 100644 --- a/app/webapi/routes/menu_layout.py +++ b/app/webapi/routes/menu_layout.py @@ -553,7 +553,7 @@ async def export_menu_layout( db: AsyncSession = Depends(get_db_session), ) -> MenuLayoutExportResponse: """Экспортировать конфигурацию меню.""" - from datetime import datetime + from datetime import UTC, datetime export_data = await MenuLayoutService.export_config(db) @@ -589,7 +589,7 @@ async def export_menu_layout( version=export_data.get('version', 1), rows=rows, buttons=buttons, - exported_at=datetime.utcnow(), + exported_at=datetime.now(UTC), ) @@ -730,12 +730,12 @@ async def get_menu_click_stats( db: AsyncSession = Depends(get_db_session), ) -> MenuClickStatsResponse: """Получить общую статистику кликов по всем кнопкам.""" - from datetime import datetime, timedelta + from datetime import UTC, datetime, timedelta stats = await MenuLayoutService.get_all_buttons_stats(db, days) total_clicks = await MenuLayoutService.get_total_clicks(db, days) - now = datetime.utcnow() + now = datetime.now(UTC) period_start = now - timedelta(days=days) return MenuClickStatsResponse( diff --git a/app/webapi/routes/miniapp.py b/app/webapi/routes/miniapp.py index b4204fb6..d92c7e5b 100644 --- a/app/webapi/routes/miniapp.py +++ b/app/webapi/routes/miniapp.py @@ -427,7 +427,7 @@ def _compute_cryptobot_limits(rate: float) -> tuple[int, int]: def _current_request_timestamp() -> str: - return datetime.utcnow().replace(microsecond=0).isoformat() + return datetime.now(UTC).replace(microsecond=0).isoformat() def _compute_stars_min_amount() -> int | None: @@ -517,7 +517,7 @@ def _parse_client_timestamp(value: str | float | None) -> datetime | None: if timestamp > 1e12: timestamp /= 1000.0 try: - return datetime.fromtimestamp(timestamp, tz=UTC).replace(tzinfo=None) + return datetime.fromtimestamp(timestamp, tz=UTC) except (OverflowError, OSError, ValueError): return None if isinstance(value, str): @@ -535,8 +535,8 @@ def _parse_client_timestamp(value: str | float | None) -> datetime | None: except ValueError: return None if parsed.tzinfo: - return parsed.astimezone(UTC).replace(tzinfo=None) - return parsed + return parsed.astimezone(UTC) + return parsed.replace(tzinfo=UTC) return None @@ -559,7 +559,7 @@ async def _find_recent_deposit( if not timestamp: return False if timestamp.tzinfo: - timestamp = timestamp.astimezone(UTC).replace(tzinfo=None) + timestamp = timestamp.astimezone(UTC) return timestamp >= reference query = ( @@ -2493,7 +2493,7 @@ async def _find_active_test_access_offers( if not subscription or not getattr(subscription, 'id', None): return [] - now = datetime.utcnow() + now = datetime.now(UTC) result = await db.execute( select(SubscriptionTemporaryAccess) .options(selectinload(SubscriptionTemporaryAccess.offer)) @@ -3204,7 +3204,7 @@ async def get_subscription_details( active_discount_percent = 0 active_discount_expires_at = getattr(user, 'promo_offer_discount_expires_at', None) - now = datetime.utcnow() + now = datetime.now(UTC) if active_discount_expires_at and active_discount_expires_at <= now: active_discount_expires_at = None active_discount_percent = 0 @@ -3478,7 +3478,7 @@ async def get_subscription_details( from app.database.models import TrafficPurchase - now = datetime.utcnow() + now = datetime.now(UTC) purchases_query = ( sql_select(TrafficPurchase) .where(TrafficPurchase.subscription_id == subscription.id) @@ -4221,7 +4221,7 @@ async def claim_promo_offer( detail={'code': 'offer_not_found', 'message': 'Offer not found'}, ) - now = datetime.utcnow() + now = datetime.now(UTC) if offer.claimed_at is not None: raise HTTPException( status.HTTP_409_CONFLICT, @@ -5923,7 +5923,7 @@ async def update_subscription_servers_endpoint( ordered_selection.append(uuid) subscription.connected_squads = ordered_selection - subscription.updated_at = datetime.utcnow() + subscription.updated_at = datetime.now(UTC) await db.commit() await db.refresh(subscription) try: @@ -6082,7 +6082,7 @@ async def update_subscription_traffic_endpoint( ) subscription.traffic_limit_gb = new_traffic - subscription.updated_at = datetime.utcnow() + subscription.updated_at = datetime.now(UTC) await db.commit() await db.refresh(subscription) try: @@ -6227,7 +6227,7 @@ async def update_subscription_devices_endpoint( ) subscription.device_limit = new_devices - subscription.updated_at = datetime.utcnow() + subscription.updated_at = datetime.now(UTC) await db.commit() await db.refresh(subscription) try: @@ -6469,7 +6469,7 @@ async def get_tariffs_endpoint( # Вычисляем оставшиеся дни подписки remaining_days = 0 if subscription and subscription.end_date: - delta = subscription.end_date - datetime.utcnow() + delta = subscription.end_date - datetime.now(UTC) remaining_days = max(0, delta.days) if current_tariff_id: @@ -6675,9 +6675,9 @@ async def purchase_tariff_endpoint( is_daily_tariff = getattr(tariff, 'is_daily', False) if is_daily_tariff: subscription.is_daily_paused = False - subscription.last_daily_charge_at = datetime.utcnow() + subscription.last_daily_charge_at = datetime.now(UTC) # Для суточного тарифа end_date = сейчас + 1 день (первый день уже оплачен) - subscription.end_date = datetime.utcnow() + timedelta(days=1) + subscription.end_date = datetime.now(UTC) + timedelta(days=1) await db.commit() await db.refresh(subscription) @@ -6850,8 +6850,8 @@ async def preview_tariff_switch_endpoint( # Рассчитываем оставшиеся дни remaining_days = 0 - if subscription.end_date and subscription.end_date > datetime.utcnow(): - delta = subscription.end_date - datetime.utcnow() + if subscription.end_date and subscription.end_date > datetime.now(UTC): + delta = subscription.end_date - datetime.now(UTC) remaining_days = max(0, delta.days) # Рассчитываем стоимость переключения @@ -6951,8 +6951,8 @@ async def switch_tariff_endpoint( # Рассчитываем оставшиеся дни remaining_days = 0 - if subscription.end_date and subscription.end_date > datetime.utcnow(): - delta = subscription.end_date - datetime.utcnow() + if subscription.end_date and subscription.end_date > datetime.now(UTC): + delta = subscription.end_date - datetime.now(UTC) remaining_days = max(0, delta.days) # Рассчитываем стоимость @@ -7037,9 +7037,9 @@ async def switch_tariff_endpoint( if new_is_daily: # Переход на суточный тариф subscription.is_daily_paused = False - subscription.last_daily_charge_at = datetime.utcnow() + subscription.last_daily_charge_at = datetime.now(UTC) # Для суточного тарифа end_date = сейчас + 1 день - subscription.end_date = datetime.utcnow() + timedelta(days=1) + subscription.end_date = datetime.now(UTC) + timedelta(days=1) logger.info('🔄 Смена на суточный тариф: установлены daily поля, end_date', end_date=subscription.end_date) elif old_is_daily and not new_is_daily: # Переход с суточного на обычный тариф - очищаем daily поля @@ -7047,7 +7047,7 @@ async def switch_tariff_endpoint( subscription.last_daily_charge_at = None # Устанавливаем дату окончания для периодного тарифа if new_period_days > 0: - subscription.end_date = datetime.utcnow() + timedelta(days=new_period_days) + subscription.end_date = datetime.now(UTC) + timedelta(days=new_period_days) logger.info( '🔄 Смена с суточного на периодный тариф: end_date= ( дней)', end_date=subscription.end_date, @@ -7325,8 +7325,8 @@ async def toggle_daily_subscription_pause_endpoint( if subscription.status == SubscriptionStatus.DISABLED.value: subscription.status = SubscriptionStatus.ACTIVE.value # Обновляем время последнего списания для корректного расчёта следующего - subscription.last_daily_charge_at = datetime.utcnow() - subscription.end_date = datetime.utcnow() + timedelta(days=1) + subscription.last_daily_charge_at = datetime.now(UTC) + subscription.end_date = datetime.now(UTC) + timedelta(days=1) logger.info('✅ Суточная подписка восстановлена из DISABLED в ACTIVE', subscription_id=subscription.id) await db.commit() diff --git a/app/webapi/routes/pinned_messages.py b/app/webapi/routes/pinned_messages.py index 8eb2c0c1..584abc93 100644 --- a/app/webapi/routes/pinned_messages.py +++ b/app/webapi/routes/pinned_messages.py @@ -1,6 +1,6 @@ from __future__ import annotations -from datetime import datetime +from datetime import UTC, datetime from typing import Any, Optional from aiogram import Bot @@ -200,7 +200,7 @@ async def update_pinned_message( if payload.send_on_every_start is not None: 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.refresh(msg) @@ -231,7 +231,7 @@ async def update_pinned_message_settings( if payload.send_on_every_start is not None: 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.refresh(msg) @@ -263,12 +263,12 @@ async def activate_pinned_message( await db.execute( update(PinnedMessage) .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.updated_at = datetime.utcnow() + msg.updated_at = datetime.now(UTC) await db.commit() await db.refresh(msg) diff --git a/app/webapi/routes/promocodes.py b/app/webapi/routes/promocodes.py index 5ebb4c84..52b3ec1e 100644 --- a/app/webapi/routes/promocodes.py +++ b/app/webapi/routes/promocodes.py @@ -37,10 +37,10 @@ def _normalize_datetime(value: datetime | None) -> datetime | None: return 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: - return value.replace(tzinfo=None) + return value return value diff --git a/app/webapi/routes/remnawave.py b/app/webapi/routes/remnawave.py index a09d65ae..4d2e1390 100644 --- a/app/webapi/routes/remnawave.py +++ b/app/webapi/routes/remnawave.py @@ -1,6 +1,6 @@ from __future__ import annotations -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from typing import TYPE_CHECKING, Any from fastapi import APIRouter, Depends, HTTPException, Query, Security, status @@ -225,7 +225,7 @@ async def get_node_usage_range( service = _get_service() _ensure_service_configured(service) - end_dt = end or datetime.utcnow() + end_dt = end or datetime.now(UTC) start_dt = start or (end_dt - timedelta(days=7)) if start_dt >= end_dt: diff --git a/app/webapi/routes/stats.py b/app/webapi/routes/stats.py index f553319f..6f0a95c0 100644 --- a/app/webapi/routes/stats.py +++ b/app/webapi/routes/stats.py @@ -1,6 +1,6 @@ from __future__ import annotations -from datetime import datetime +from datetime import UTC, datetime from fastapi import APIRouter, Depends, Security from sqlalchemy import func, select @@ -73,7 +73,7 @@ async def _get_overview(db: AsyncSession) -> dict[str, object]: or 0 ) - today = datetime.utcnow().date() + today = datetime.now(UTC).date() today_transactions = ( await db.scalar( select(func.coalesce(func.sum(Transaction.amount_kopeks), 0)).where( diff --git a/app/webapi/routes/tickets.py b/app/webapi/routes/tickets.py index edc598c0..a66f7a2a 100644 --- a/app/webapi/routes/tickets.py +++ b/app/webapi/routes/tickets.py @@ -1,6 +1,6 @@ from __future__ import annotations -from datetime import datetime +from datetime import UTC, datetime from typing import Any import structlog @@ -121,7 +121,7 @@ async def update_ticket_status( except ValueError as error: raise HTTPException(status.HTTP_400_BAD_REQUEST, 'Invalid ticket status') from error - closed_at = datetime.utcnow() if status_value == TicketStatus.CLOSED.value else None + closed_at = datetime.now(UTC) if status_value == TicketStatus.CLOSED.value else None success = await TicketCRUD.update_ticket_status(db, ticket_id, status_value, closed_at) if not success: raise HTTPException(status.HTTP_404_NOT_FOUND, 'Ticket not found') @@ -146,7 +146,7 @@ async def update_ticket_priority( raise HTTPException(status.HTTP_404_NOT_FOUND, 'Ticket not found') ticket.priority = payload.priority - ticket.updated_at = datetime.utcnow() + ticket.updated_at = datetime.now(UTC) await db.commit() ticket = await TicketCRUD.get_ticket_by_id(db, ticket_id, load_messages=True, load_user=False) @@ -161,7 +161,7 @@ async def update_reply_block( db: AsyncSession = Depends(get_db_session), ) -> TicketResponse: until = payload.until - if not payload.permanent and until and until <= datetime.utcnow(): + if not payload.permanent and until and until <= datetime.now(UTC): raise HTTPException(status.HTTP_400_BAD_REQUEST, 'Block expiration must be in the future') success = await TicketCRUD.set_user_reply_block( diff --git a/pyproject.toml b/pyproject.toml index 7e9d0ac4..9b04d0ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -149,7 +149,6 @@ ignore = [ 'B904', 'TID252', 'DTZ001', - 'DTZ003', 'DTZ005', 'FAST001', 'FAST002', diff --git a/tests/conftest.py b/tests/conftest.py index 308287d0..0e17fae0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -88,7 +88,7 @@ if 'yookassa' not in sys.modules: metadata = {} amount = types.SimpleNamespace(value='0.00', currency='RUB') confirmation = types.SimpleNamespace(confirmation_url='https://example.com') - created_at = datetime.utcnow() + created_at = datetime.now(UTC) description = '' test = False diff --git a/tests/fixtures/promocode_fixtures.py b/tests/fixtures/promocode_fixtures.py index 3059c277..ab2e2711 100644 --- a/tests/fixtures/promocode_fixtures.py +++ b/tests/fixtures/promocode_fixtures.py @@ -2,7 +2,7 @@ Fixtures for promocode and promo group testing """ -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from types import SimpleNamespace from unittest.mock import AsyncMock @@ -59,8 +59,8 @@ def sample_promocode_balance(): promo_group_id=None, promo_group=None, valid_until=None, - created_at=datetime.utcnow(), - updated_at=datetime.utcnow(), + created_at=datetime.now(UTC), + updated_at=datetime.now(UTC), created_by=1, ) @@ -79,9 +79,9 @@ def sample_promocode_subscription(): is_active=True, promo_group_id=None, promo_group=None, - valid_until=datetime.utcnow() + timedelta(days=60), - created_at=datetime.utcnow(), - updated_at=datetime.utcnow(), + valid_until=datetime.now(UTC) + timedelta(days=60), + created_at=datetime.now(UTC), + updated_at=datetime.now(UTC), created_by=1, ) @@ -101,8 +101,8 @@ def sample_promocode_promo_group(sample_promo_group): promo_group_id=sample_promo_group.id, promo_group=sample_promo_group, valid_until=None, - created_at=datetime.utcnow(), - updated_at=datetime.utcnow(), + created_at=datetime.now(UTC), + updated_at=datetime.now(UTC), created_by=1, ) @@ -121,9 +121,9 @@ def sample_promocode_invalid(): is_active=False, promo_group_id=None, promo_group=None, - valid_until=datetime.utcnow() - timedelta(days=1), # Expired - created_at=datetime.utcnow() - timedelta(days=30), - updated_at=datetime.utcnow(), + valid_until=datetime.now(UTC) - timedelta(days=1), # Expired + created_at=datetime.now(UTC) - timedelta(days=30), + updated_at=datetime.now(UTC), created_by=1, ) diff --git a/tests/services/test_pal24_service_adapter.py b/tests/services/test_pal24_service_adapter.py index 492127bc..4554f69f 100644 --- a/tests/services/test_pal24_service_adapter.py +++ b/tests/services/test_pal24_service_adapter.py @@ -3,7 +3,7 @@ from __future__ import annotations import sys -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from decimal import Decimal from pathlib import Path from typing import Any @@ -134,4 +134,4 @@ def test_convert_to_kopeks_and_expiration() -> None: assert Pal24Service.convert_to_kopeks('10.50') == 1050 expiration = Pal24Service.get_expiration(60) assert isinstance(expiration, datetime) - assert expiration - datetime.utcnow() <= timedelta(seconds=61) + assert expiration - datetime.now(UTC) <= timedelta(seconds=61) diff --git a/tests/services/test_payment_service_cryptobot.py b/tests/services/test_payment_service_cryptobot.py index fb29d45d..397cfa6a 100644 --- a/tests/services/test_payment_service_cryptobot.py +++ b/tests/services/test_payment_service_cryptobot.py @@ -1,7 +1,7 @@ """Тесты сценариев CryptoBot в PaymentService.""" import sys -from datetime import datetime +from datetime import UTC, datetime from pathlib import Path from typing import Any @@ -39,7 +39,7 @@ class DummySession: class DummyLocalPayment: def __init__(self, payment_id: int = 888) -> None: self.id = payment_id - self.created_at = datetime(2024, 3, 1, 9, 0, 0) + self.created_at = datetime(2024, 3, 1, 9, 0, 0, tzinfo=UTC) class StubCryptoBotService: diff --git a/tests/services/test_payment_service_heleket.py b/tests/services/test_payment_service_heleket.py index c5918d4c..7b20f4e8 100644 --- a/tests/services/test_payment_service_heleket.py +++ b/tests/services/test_payment_service_heleket.py @@ -1,5 +1,5 @@ import sys -from datetime import datetime +from datetime import UTC, datetime from pathlib import Path from types import SimpleNamespace from typing import Any @@ -37,7 +37,7 @@ class DummySession: class DummyLocalPayment: def __init__(self, payment_id: int = 123) -> None: self.id = payment_id - self.created_at = datetime(2024, 1, 1, 12, 0, 0) + self.created_at = datetime(2024, 1, 1, 12, 0, 0, tzinfo=UTC) class StubHeleketService: diff --git a/tests/services/test_payment_service_mulenpay.py b/tests/services/test_payment_service_mulenpay.py index 1099caf6..f0ac3bb7 100644 --- a/tests/services/test_payment_service_mulenpay.py +++ b/tests/services/test_payment_service_mulenpay.py @@ -1,7 +1,7 @@ """Тесты для сценариев MulenPay в PaymentService.""" import sys -from datetime import datetime +from datetime import UTC, datetime from pathlib import Path from types import ModuleType, SimpleNamespace from typing import Any @@ -34,7 +34,7 @@ class DummySession: class DummyLocalPayment: def __init__(self, payment_id: int = 501) -> None: self.id = payment_id - self.created_at = datetime(2024, 1, 1, 12, 0, 0) + self.created_at = datetime(2024, 1, 1, 12, 0, 0, tzinfo=UTC) class StubMulenPayService: diff --git a/tests/services/test_payment_service_pal24.py b/tests/services/test_payment_service_pal24.py index 1e7d2576..92012e95 100644 --- a/tests/services/test_payment_service_pal24.py +++ b/tests/services/test_payment_service_pal24.py @@ -1,7 +1,7 @@ """Тесты Pal24 сценариев PaymentService.""" import sys -from datetime import datetime +from datetime import UTC, datetime from pathlib import Path from types import SimpleNamespace from typing import Any @@ -32,7 +32,7 @@ class DummySession: class DummyLocalPayment: def __init__(self, payment_id: int = 404) -> None: self.id = payment_id - self.created_at = datetime(2024, 1, 2, 10, 0, 0) + self.created_at = datetime(2024, 1, 2, 10, 0, 0, tzinfo=UTC) class StubPal24Service: diff --git a/tests/services/test_payment_service_platega.py b/tests/services/test_payment_service_platega.py index 42711d27..00074290 100644 --- a/tests/services/test_payment_service_platega.py +++ b/tests/services/test_payment_service_platega.py @@ -3,7 +3,7 @@ from __future__ import annotations import sys -from datetime import datetime +from datetime import UTC, datetime from pathlib import Path from typing import Any @@ -35,7 +35,7 @@ class DummySession: class DummyLocalPayment: def __init__(self, payment_id: int = 101) -> None: self.id = payment_id - self.created_at = datetime.utcnow() + self.created_at = datetime.now(UTC) class StubPlategaService: diff --git a/tests/services/test_payment_service_stars.py b/tests/services/test_payment_service_stars.py index 1e39bd7a..07ff2aaf 100644 --- a/tests/services/test_payment_service_stars.py +++ b/tests/services/test_payment_service_stars.py @@ -1,7 +1,7 @@ """Тесты для Telegram Stars-сценариев внутри PaymentService.""" import sys -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from decimal import Decimal from pathlib import Path from typing import Any @@ -89,7 +89,7 @@ class DummySubscription: self.traffic_limit_gb = traffic_limit_gb self.device_limit = device_limit self.status = 'pending' - self.start_date = datetime(2024, 1, 1) + self.start_date = datetime(2024, 1, 1, tzinfo=UTC) self.end_date = self.start_date + timedelta(days=period_days) diff --git a/tests/services/test_payment_service_wata.py b/tests/services/test_payment_service_wata.py index f3612796..448907b3 100644 --- a/tests/services/test_payment_service_wata.py +++ b/tests/services/test_payment_service_wata.py @@ -3,7 +3,7 @@ from __future__ import annotations import sys -from datetime import datetime +from datetime import UTC, datetime from pathlib import Path from typing import Any @@ -36,7 +36,7 @@ class DummySession: class DummyLocalPayment: def __init__(self, payment_id: int = 42) -> None: self.id = payment_id - self.created_at = datetime.utcnow() + self.created_at = datetime.now(UTC) class StubWataService: @@ -87,12 +87,12 @@ def test_wata_service_format_datetime_accepts_naive_utc() -> None: def test_wata_service_parse_datetime_returns_naive_utc() -> None: parsed = WataService._parse_datetime('2024-05-20T12:30:00Z') - assert parsed == datetime(2024, 5, 20, 12, 30, 0) - assert parsed.tzinfo is None + assert parsed == datetime(2024, 5, 20, 12, 30, 0, tzinfo=UTC) + assert parsed.tzinfo is not None parsed_with_offset = WataService._parse_datetime('2024-05-20T15:30:00+03:00') - assert parsed_with_offset == datetime(2024, 5, 20, 12, 30, 0) - assert parsed_with_offset.tzinfo is None + assert parsed_with_offset == datetime(2024, 5, 20, 12, 30, 0, tzinfo=UTC) + assert parsed_with_offset.tzinfo is not None @pytest.mark.anyio('asyncio') diff --git a/tests/services/test_payment_service_yookassa.py b/tests/services/test_payment_service_yookassa.py index 88faa8a8..1f372530 100644 --- a/tests/services/test_payment_service_yookassa.py +++ b/tests/services/test_payment_service_yookassa.py @@ -1,7 +1,7 @@ """Тесты для YooKassa-сценариев PaymentService.""" import sys -from datetime import datetime +from datetime import UTC, datetime from pathlib import Path from typing import Any @@ -41,7 +41,7 @@ class DummyLocalPayment: def __init__(self, payment_id: int = 101) -> None: self.id = payment_id - self.created_at = datetime(2024, 1, 1, 12, 0, 0) + self.created_at = datetime(2024, 1, 1, 12, 0, 0, tzinfo=UTC) class StubYooKassaService: diff --git a/tests/services/test_referral_diagnostics.py b/tests/services/test_referral_diagnostics.py index 1298d67c..902252da 100644 --- a/tests/services/test_referral_diagnostics.py +++ b/tests/services/test_referral_diagnostics.py @@ -3,7 +3,7 @@ """ import tempfile -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from pathlib import Path import pytest @@ -23,7 +23,7 @@ def temp_log_file(): @pytest.fixture def sample_log_content(): """Пример содержимого лог-файла с реферальными событиями.""" - today = datetime.now().strftime('%Y-%m-%d') + today = datetime.now(UTC).strftime('%Y-%m-%d') return f""" {today} 10:00:00,123 - app.handlers.start - INFO - 🔎 Найден реферальный код: {today} 10:00:05,456 - app.handlers.start - INFO - ✅ Реферальный код ABC123 применен для пользователя 123456789 @@ -47,7 +47,7 @@ async def test_parse_logs_basic(temp_log_file, sample_log_content): service = ReferralDiagnosticsService(log_path=str(temp_log_file)) - today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) + today = datetime.now(UTC).replace(hour=0, minute=0, second=0, microsecond=0) tomorrow = today + timedelta(days=1) events = await service._parse_logs(today, tomorrow) @@ -70,7 +70,7 @@ async def test_analyze_period_with_issues(temp_log_file, sample_log_content): service = ReferralDiagnosticsService(log_path=str(temp_log_file)) - today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) + today = datetime.now(UTC).replace(hour=0, minute=0, second=0, microsecond=0) tomorrow = today + timedelta(days=1) # Используем None вместо db для базового теста парсинга @@ -100,7 +100,7 @@ async def test_empty_log_file(temp_log_file): service = ReferralDiagnosticsService(log_path=str(temp_log_file)) - today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) + today = datetime.now(UTC).replace(hour=0, minute=0, second=0, microsecond=0) tomorrow = today + timedelta(days=1) from unittest.mock import AsyncMock @@ -121,7 +121,7 @@ async def test_nonexistent_log_file(): """Тест работы с несуществующим лог-файлом.""" service = ReferralDiagnosticsService(log_path='/nonexistent/path/to/log.log') - today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) + today = datetime.now(UTC).replace(hour=0, minute=0, second=0, microsecond=0) tomorrow = today + timedelta(days=1) from unittest.mock import AsyncMock @@ -149,5 +149,5 @@ async def test_analyze_today(temp_log_file, sample_log_content): report = await service.analyze_today(mock_db) # Проверяем что период установлен корректно - today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) + today = datetime.now(UTC).replace(hour=0, minute=0, second=0, microsecond=0) assert report.analysis_period_start.date() == today.date() diff --git a/tests/services/test_subscription_auto_purchase_service.py b/tests/services/test_subscription_auto_purchase_service.py index 6684378a..8e73231d 100644 --- a/tests/services/test_subscription_auto_purchase_service.py +++ b/tests/services/test_subscription_auto_purchase_service.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from unittest.mock import AsyncMock, MagicMock from sqlalchemy.ext.asyncio import AsyncSession @@ -197,7 +197,7 @@ async def test_auto_purchase_saved_cart_after_topup_extension(monkeypatch): subscription.id = 99 subscription.is_trial = False subscription.status = 'active' - subscription.end_date = datetime.utcnow() + subscription.end_date = datetime.now(UTC) subscription.device_limit = 1 subscription.traffic_limit_gb = 100 subscription.connected_squads = ['squad-a'] @@ -319,7 +319,7 @@ async def test_auto_purchase_trial_preserved_on_insufficient_balance(monkeypatch subscription.id = 123 subscription.is_trial = True # Триальная подписка! subscription.status = 'active' - subscription.end_date = datetime.utcnow() + timedelta(days=2) # Осталось 2 дня + subscription.end_date = datetime.now(UTC) + timedelta(days=2) # Осталось 2 дня subscription.device_limit = 1 subscription.traffic_limit_gb = 10 subscription.connected_squads = [] @@ -403,7 +403,7 @@ async def test_auto_purchase_trial_converted_after_successful_extension(monkeypa subscription.id = 456 subscription.is_trial = True # Триальная подписка! subscription.status = 'active' - subscription.end_date = datetime.utcnow() + timedelta(days=1) + subscription.end_date = datetime.now(UTC) + timedelta(days=1) subscription.device_limit = 1 subscription.traffic_limit_gb = 10 subscription.connected_squads = [] @@ -519,7 +519,7 @@ async def test_auto_purchase_trial_preserved_on_extension_failure(monkeypatch): subscription.id = 789 subscription.is_trial = True # Триальная подписка! subscription.status = 'active' - subscription.end_date = datetime.utcnow() + timedelta(days=3) + subscription.end_date = datetime.now(UTC) + timedelta(days=3) subscription.device_limit = 1 subscription.traffic_limit_gb = 10 subscription.connected_squads = [] @@ -610,7 +610,7 @@ async def test_auto_purchase_trial_remaining_days_transferred(monkeypatch): monkeypatch.setattr(settings, 'AUTO_PURCHASE_AFTER_TOPUP_ENABLED', True) monkeypatch.setattr(settings, 'TRIAL_ADD_REMAINING_DAYS_TO_PAID', True) # Включено! - now = datetime.utcnow() + now = datetime.now(UTC) trial_end = now + timedelta(days=2) # Осталось 2 дня триала subscription = MagicMock() diff --git a/tests/services/test_traffic_monitoring_redis.py b/tests/services/test_traffic_monitoring_redis.py index 3fb9d5af..9bd1c084 100644 --- a/tests/services/test_traffic_monitoring_redis.py +++ b/tests/services/test_traffic_monitoring_redis.py @@ -2,7 +2,7 @@ Тесты для хранения snapshot трафика в Redis. """ -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -120,7 +120,7 @@ async def test_load_snapshot_from_redis_exception(service, mock_cache): async def test_get_snapshot_time_from_redis_success(service, mock_cache): """Тест получения времени snapshot.""" - test_time = datetime(2024, 1, 15, 12, 30, 0) + test_time = datetime(2024, 1, 15, 12, 30, 0, tzinfo=UTC) mock_cache.get = AsyncMock(return_value=test_time.isoformat()) result = await service._get_snapshot_time_from_redis() @@ -156,7 +156,7 @@ async def test_has_snapshot_memory_fallback(service, mock_cache): # Устанавливаем данные в память service._memory_snapshot = {'uuid-1': 1000.0} - service._memory_snapshot_time = datetime.utcnow() + service._memory_snapshot_time = datetime.now(UTC) result = await service.has_snapshot() @@ -180,7 +180,7 @@ async def test_has_snapshot_none(service, mock_cache): async def test_get_snapshot_age_minutes_from_redis(service, mock_cache): """Тест возраста snapshot из Redis.""" # Snapshot создан 30 минут назад - past_time = datetime.utcnow() - timedelta(minutes=30) + past_time = datetime.now(UTC) - timedelta(minutes=30) mock_cache.get = AsyncMock(return_value=past_time.isoformat()) result = await service.get_snapshot_age_minutes() @@ -191,7 +191,7 @@ async def test_get_snapshot_age_minutes_from_redis(service, mock_cache): async def test_get_snapshot_age_minutes_memory_fallback(service, mock_cache): """Тест возраста snapshot из памяти.""" mock_cache.get = AsyncMock(return_value=None) - service._memory_snapshot_time = datetime.utcnow() - timedelta(minutes=15) + service._memory_snapshot_time = datetime.now(UTC) - timedelta(minutes=15) result = await service.get_snapshot_age_minutes() @@ -217,7 +217,7 @@ async def test_save_snapshot_redis_success(service, mock_cache, sample_snapshot) # Заполняем память чтобы проверить что она очистится service._memory_snapshot = {'old': 123.0} - service._memory_snapshot_time = datetime.utcnow() + service._memory_snapshot_time = datetime.now(UTC) result = await service._save_snapshot(sample_snapshot) @@ -276,7 +276,7 @@ async def test_save_notification_to_redis(service, mock_cache): async def test_get_notification_time_from_redis(service, mock_cache): """Тест получения времени уведомления.""" - test_time = datetime(2024, 1, 15, 10, 0, 0) + test_time = datetime(2024, 1, 15, 10, 0, 0, tzinfo=UTC) mock_cache.get = AsyncMock(return_value=test_time.isoformat()) result = await service._get_notification_time_from_redis('uuid-123') @@ -297,7 +297,7 @@ async def test_should_send_notification_no_previous(service, mock_cache): async def test_should_send_notification_cooldown_active(service, mock_cache): """Тест should_send_notification когда кулдаун активен.""" # Уведомление было 5 минут назад, кулдаун 60 минут - recent_time = datetime.utcnow() - timedelta(minutes=5) + recent_time = datetime.now(UTC) - timedelta(minutes=5) mock_cache.get = AsyncMock(return_value=recent_time.isoformat()) result = await service.should_send_notification('uuid-123') @@ -308,7 +308,7 @@ async def test_should_send_notification_cooldown_active(service, mock_cache): async def test_should_send_notification_cooldown_expired(service, mock_cache): """Тест should_send_notification когда кулдаун истёк.""" # Уведомление было 120 минут назад, кулдаун 60 минут - old_time = datetime.utcnow() - timedelta(minutes=120) + old_time = datetime.now(UTC) - timedelta(minutes=120) mock_cache.get = AsyncMock(return_value=old_time.isoformat()) result = await service.should_send_notification('uuid-123') @@ -342,7 +342,7 @@ async def test_create_initial_snapshot_uses_existing_redis(service, mock_cache, mock_cache.get = AsyncMock( side_effect=[ sample_snapshot, # _load_snapshot_from_redis - (datetime.utcnow() - timedelta(minutes=10)).isoformat(), # _get_snapshot_time_from_redis + (datetime.now(UTC) - timedelta(minutes=10)).isoformat(), # _get_snapshot_time_from_redis ] ) @@ -379,8 +379,8 @@ async def test_create_initial_snapshot_creates_new(service, mock_cache): async def test_cleanup_notification_cache_removes_old(service, mock_cache): """Тест очистки старых записей из памяти.""" - old_time = datetime.utcnow() - timedelta(hours=25) - recent_time = datetime.utcnow() - timedelta(hours=1) + old_time = datetime.now(UTC) - timedelta(hours=25) + recent_time = datetime.now(UTC) - timedelta(hours=1) service._memory_notification_cache = { 'uuid-old': old_time, diff --git a/tests/services/test_yookassa_service_adapter.py b/tests/services/test_yookassa_service_adapter.py index 2d1dca5e..e683ece5 100644 --- a/tests/services/test_yookassa_service_adapter.py +++ b/tests/services/test_yookassa_service_adapter.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio import sys -from datetime import datetime +from datetime import UTC, datetime from pathlib import Path from types import SimpleNamespace @@ -69,7 +69,7 @@ async def test_create_payment_success(monkeypatch: pytest.MonkeyPatch) -> None: metadata={'meta': 'value'}, amount=SimpleNamespace(value='140.00', currency='RUB'), refundable=True, - created_at=datetime(2024, 1, 1, 12, 0, 0), + created_at=datetime(2024, 1, 1, 12, 0, 0, tzinfo=UTC), description='Desc', test=False, ) @@ -154,7 +154,7 @@ async def test_create_sbp_payment_success(monkeypatch: pytest.MonkeyPatch) -> No metadata={'meta': 'value'}, amount=SimpleNamespace(value='200.00', currency='RUB'), refundable=False, - created_at=datetime(2024, 2, 1, 9, 0, 0), + created_at=datetime(2024, 2, 1, 9, 0, 0, tzinfo=UTC), description='SBP payment', test=True, ) diff --git a/tests/test_menu_subscription_status.py b/tests/test_menu_subscription_status.py index 91217a9a..a90c3226 100644 --- a/tests/test_menu_subscription_status.py +++ b/tests/test_menu_subscription_status.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from unittest.mock import MagicMock from app.handlers.menu import _get_subscription_status @@ -13,7 +13,7 @@ def _build_user_with_subscription(actual_status: str, is_trial: bool, days_left: subscription = MagicMock() subscription.actual_status = actual_status subscription.is_trial = is_trial - subscription.end_date = datetime.utcnow() + timedelta(days=days_left, hours=1) + subscription.end_date = datetime.now(UTC) + timedelta(days=days_left, hours=1) user = MagicMock() user.subscription = subscription diff --git a/tests/test_miniapp_payments.py b/tests/test_miniapp_payments.py index 7188b5c9..00361f89 100644 --- a/tests/test_miniapp_payments.py +++ b/tests/test_miniapp_payments.py @@ -1,7 +1,7 @@ import os import sys import types -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from pathlib import Path from typing import Any @@ -105,7 +105,7 @@ async def test_submit_subscription_renewal_uses_balance_when_sufficient(monkeypa connected_squads=[], traffic_limit_gb=100, device_limit=5, - end_date=datetime.utcnow(), + end_date=datetime.now(UTC), ) pricing_model = SubscriptionRenewalPricing( @@ -139,7 +139,7 @@ async def test_submit_subscription_renewal_uses_balance_when_sufficient(monkeypa captured['charge'] = charge captured['description'] = description return SubscriptionRenewalResult( - subscription=types.SimpleNamespace(id=sub.id, end_date=datetime.utcnow()), + subscription=types.SimpleNamespace(id=sub.id, end_date=datetime.now(UTC)), transaction=types.SimpleNamespace(id=501), total_amount_kopeks=pricing.final_total, charged_from_balance_kopeks=charge, @@ -185,7 +185,7 @@ async def test_submit_subscription_renewal_returns_cryptobot_invoice(monkeypatch connected_squads=[], traffic_limit_gb=100, device_limit=5, - end_date=datetime.utcnow(), + end_date=datetime.now(UTC), ) pricing_model = SubscriptionRenewalPricing( @@ -276,7 +276,7 @@ async def test_submit_subscription_renewal_rounds_up_cryptobot_amount(monkeypatc connected_squads=[], traffic_limit_gb=100, device_limit=5, - end_date=datetime.utcnow(), + end_date=datetime.now(UTC), ) pricing_model = SubscriptionRenewalPricing( @@ -397,7 +397,7 @@ async def test_cryptobot_renewal_uses_pricing_snapshot(monkeypatch): captured['description'] = description captured['payment_method'] = payment_method return SubscriptionRenewalResult( - subscription=types.SimpleNamespace(id=sub.id, end_date=datetime.utcnow()), + subscription=types.SimpleNamespace(id=sub.id, end_date=datetime.now(UTC)), transaction=types.SimpleNamespace(id=999), total_amount_kopeks=pricing.final_total, charged_from_balance_kopeks=charge_balance_amount or pricing.final_total, @@ -477,7 +477,7 @@ async def test_cryptobot_renewal_accepts_changed_pricing_without_snapshot(monkey captured['pricing'] = pricing captured['charge'] = charge_balance_amount return SubscriptionRenewalResult( - subscription=types.SimpleNamespace(id=sub.id, end_date=datetime.utcnow()), + subscription=types.SimpleNamespace(id=sub.id, end_date=datetime.now(UTC)), transaction=None, total_amount_kopeks=pricing.final_total, charged_from_balance_kopeks=charge_balance_amount or pricing.final_total, @@ -572,7 +572,7 @@ async def test_cryptobot_webhook_uses_inline_payload_when_db_missing(monkeypatch captured['description'] = description captured['payment_method'] = payment_method return SubscriptionRenewalResult( - subscription=types.SimpleNamespace(id=sub.id, end_date=datetime.utcnow()), + subscription=types.SimpleNamespace(id=sub.id, end_date=datetime.now(UTC)), transaction=types.SimpleNamespace(id=1234), total_amount_kopeks=pricing.final_total, charged_from_balance_kopeks=charge_balance_amount or pricing.final_total, @@ -733,7 +733,7 @@ async def test_resolve_yookassa_status_includes_identifiers(monkeypatch): is_paid=False, captured_at=None, updated_at=None, - created_at=datetime.utcnow(), + created_at=datetime.now(UTC), transaction_id=42, yookassa_payment_id='yk_1', ) @@ -780,7 +780,7 @@ async def test_resolve_payment_status_supports_yookassa_sbp(monkeypatch): is_paid=False, captured_at=None, updated_at=None, - created_at=datetime.utcnow(), + created_at=datetime.now(UTC), transaction_id=None, yookassa_payment_id='yk_sbp_1', ) @@ -831,7 +831,7 @@ async def test_resolve_pal24_status_includes_identifiers(monkeypatch): ) monkeypatch.setitem(sys.modules, 'app.database.crud.pal24', stub_module) - paid_at = datetime.utcnow() + paid_at = datetime.now(UTC) payment = types.SimpleNamespace( id=321, @@ -886,7 +886,7 @@ async def test_resolve_pal24_status_includes_identifiers(monkeypatch): @pytest.mark.anyio('asyncio') async def test_resolve_wata_payment_status_success(): - paid_at = datetime.utcnow() + paid_at = datetime.now(UTC) payment = types.SimpleNamespace( id=404, user_id=9, @@ -938,7 +938,7 @@ async def test_resolve_wata_payment_status_success(): @pytest.mark.anyio('asyncio') async def test_resolve_wata_payment_status_uses_payment_link_lookup(monkeypatch): - created_at = datetime.utcnow() + created_at = datetime.now(UTC) payment = types.SimpleNamespace( id=505, user_id=7, diff --git a/tests/utils/test_formatters_basic.py b/tests/utils/test_formatters_basic.py index 604301e0..db1f418c 100644 --- a/tests/utils/test_formatters_basic.py +++ b/tests/utils/test_formatters_basic.py @@ -1,6 +1,6 @@ """Тесты для базовых форматтеров из app.utils.formatters.""" -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from app.utils import formatters @@ -19,7 +19,7 @@ def test_format_date_uses_custom_format(fixed_datetime: datetime) -> None: def test_format_time_ago_returns_human_readable_text() -> None: """Разница во времени должна переводиться в человеко-понятную строку.""" - point_in_time = datetime.utcnow() - timedelta(minutes=5) + point_in_time = datetime.now(UTC) - timedelta(minutes=5) assert formatters.format_time_ago(point_in_time, language='ru') == '5 мин. назад' assert formatters.format_time_ago(point_in_time, language='en') == '5 minutes ago' @@ -78,7 +78,7 @@ def test_format_username_prefers_full_name() -> None: def test_format_subscription_status_handles_active_and_expired() -> None: """Статус подписки различается для активных/просроченных случаев.""" - future = datetime.utcnow() + timedelta(days=2) + future = datetime.now(UTC) + timedelta(days=2) active = formatters.format_subscription_status( is_active=True, is_trial=False, @@ -88,7 +88,7 @@ def test_format_subscription_status_handles_active_and_expired() -> None: assert active.startswith('✅ Активна') assert '(' in active and ')' in active - past = datetime.utcnow() - timedelta(days=1) + past = datetime.now(UTC) - timedelta(days=1) expired = formatters.format_subscription_status( is_active=True, is_trial=False, diff --git a/tests/utils/test_remnawave_auto_sync.py b/tests/utils/test_remnawave_auto_sync.py index 58f19b81..0448eda8 100644 --- a/tests/utils/test_remnawave_auto_sync.py +++ b/tests/utils/test_remnawave_auto_sync.py @@ -1,6 +1,6 @@ import asyncio from collections import deque -from datetime import datetime, time as time_cls +from datetime import UTC, datetime, time as time_cls from types import SimpleNamespace from unittest.mock import AsyncMock @@ -30,30 +30,30 @@ def _patch_datetime(monkeypatch, current): monkeypatch.setattr( 'app.services.remnawave_sync_service.datetime', SimpleNamespace( - utcnow=lambda: current, - combine=lambda date_obj, time_obj: real_datetime.combine(date_obj, time_obj), + now=lambda tz=None: current, + combine=lambda date_obj, time_obj, tzinfo=None: real_datetime.combine(date_obj, time_obj, tzinfo=tzinfo), ), ) def test_calculate_next_run_same_day(monkeypatch): service = RemnaWaveAutoSyncService() - current = datetime(2024, 1, 1, 2, 30) + current = datetime(2024, 1, 1, 2, 30, tzinfo=UTC) _patch_datetime(monkeypatch, current) next_run = service._calculate_next_run([time_cls(1, 0), time_cls(3, 0)]) - assert next_run == datetime(2024, 1, 1, 3, 0) + assert next_run == datetime(2024, 1, 1, 3, 0, tzinfo=UTC) def test_calculate_next_run_rollover(monkeypatch): service = RemnaWaveAutoSyncService() - current = datetime(2024, 1, 1, 23, 45) + current = datetime(2024, 1, 1, 23, 45, tzinfo=UTC) _patch_datetime(monkeypatch, current) next_run = service._calculate_next_run([time_cls(1, 0), time_cls(10, 0)]) - assert next_run == datetime(2024, 1, 2, 1, 0) + assert next_run == datetime(2024, 1, 2, 1, 0, tzinfo=UTC) def test_perform_sync_rebuilds_service_on_each_run(monkeypatch):