Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] e375252d65 ci(deps): bump the github-actions group across 1 directory with 3 updates
Bumps the github-actions group with 3 updates in the / directory: [actions/checkout](https://github.com/actions/checkout), [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) and [actions/setup-python](https://github.com/actions/setup-python).


Updates `actions/checkout` from 4 to 6
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

Updates `astral-sh/setup-uv` from 5 to 7
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/v5...v7)

Updates `actions/setup-python` from 5 to 6
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: astral-sh/setup-uv
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 07:12:26 +00:00
200 changed files with 1428 additions and 1681 deletions
-2
View File
@@ -845,8 +845,6 @@ VERSION_CHECK_INTERVAL_HOURS=1
# ===== ЛОГИРОВАНИЕ =====
LOG_LEVEL=INFO
LOG_FILE=logs/bot.log
# ANSI-цвета в консоли (true — цветной вывод с Rich, false — plain-text)
LOG_COLORS=true
# === Ротация логов ===
# Включить новую систему ротации (по умолчанию старое поведение)
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
+3 -3
View File
@@ -10,11 +10,11 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v5
- uses: astral-sh/setup-uv@v7
- uses: actions/setup-python@v5
- uses: actions/setup-python@v6
with:
python-version: '3.13'
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "3.15.1"
".": "3.12.1"
}
-76
View File
@@ -1,81 +1,5 @@
# Changelog
## [3.15.1](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/compare/v3.15.0...v3.15.1) (2026-02-17)
### Bug Fixes
* add naive datetime guards to fromisoformat() in Redis cache readers ([1b3e6f2](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/1b3e6f2f11c20aa240da1beb11dd7dfb20dbe6e8))
* add naive datetime guards to fromisoformat() in Redis cache readers ([6fa4948](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/6fa49485d9f1cd678cb5f9fa7d0375fd47643239))
## [3.15.0](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/compare/v3.14.1...v3.15.0) (2026-02-17)
### New Features
* add LOG_COLORS env setting to toggle console ANSI colors ([27309f5](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/27309f53d9fa0ba9a2ca07a65feed96bf38f470c))
* add web campaign links with bonus processing in auth flow ([d955279](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/d9552799c17a76e2cc2118699528c5b591bd97fb))
### Bug Fixes
* AttributeError in withdrawal admin notification (send_to_admins → send_admin_notification) ([c75ec0b](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/c75ec0b22a3f674d3e1a24b9d546eca1998701b3))
* remove local UTC re-imports shadowing module-level import in purchase.py ([e68760c](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/e68760cc668016209f4f19a2e08af8680343d6ed))
## [3.14.1](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/compare/v3.14.0...v3.14.1) (2026-02-17)
### Bug Fixes
* add naive datetime guards to parsers and fix test datetime literals ([0946090](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/094609005af7358bf5d34d252fc66685bd25751c))
* address remaining abs() issues from review ([ff21b27](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/ff21b27b98bb5a7517e06057eb319c9f3ebb74c7))
* complete datetime.utcnow() → datetime.now(UTC) migration ([eb18994](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/eb18994b7d34d777ca39d3278d509e41359e2a85))
* normalize transaction amount signs across all aggregations ([4247981](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/4247981c98111af388c98628c1e61f0517c57417))
* prevent negative amounts in spent display and balance history ([c30972f](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/c30972f6a7911a89a6c3f2080019ff465d11b597))
## [3.14.0](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/compare/v3.13.0...v3.14.0) (2026-02-16)
### New Features
* show all active webhook endpoints in startup log ([9d71005](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/9d710050ad40ba76a14aa6ace8e8a47f25cdde94))
### Bug Fixes
* force basicConfig to replace pre-existing handlers ([7eb8d4e](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/7eb8d4e153bab640a5829f75bfa6f70df5763284))
* NameError in set_user_devices_button — undefined action_text ([1b8ef69](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/1b8ef69a1bbb7d8d86827cf7aaa4f05cbf480d75))
* remove unused PaymentService from MonitoringService init ([491a7e1](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/491a7e1c425a355e55b3020e2bcc7b96047bdf5e))
* resolve MissingGreenlet error when accessing subscription.tariff ([a93a32f](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/a93a32f3a7d1b259a2e24954ae5d2b7c966c5639))
* sync support mode from cabinet admin to SupportSettingsService ([516be6e](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/516be6e600a08ad700d83b793dc64b2ca07bdf44))
* sync SUPPORT_SYSTEM_MODE between SystemSettings and SupportSettings ([0807a9f](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/0807a9ff19d1eb4f1204f7cbeb1da1c1cfefe83a))
### Refactoring
* improve log formatting — logger name prefix and table alignment ([f637204](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/f63720467a935bdaaa58bb34d588d65e46698f26))
## [3.13.0](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/compare/v3.12.1...v3.13.0) (2026-02-16)
### New Features
* colored console logs via structlog + rich + FORCE_COLOR ([bf64611](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/bf646112df02aa7aa7918d0513cb6968ceb7f378))
### Bug Fixes
* limit Rich traceback output to prevent console flood ([11ef714](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/11ef714e0dde25a08711c0daeee943b6e71e20b7))
* resolve exc_info for admin notifications, clean log formatting ([11f8af0](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/11f8af003fc60384abafa2b670b89d6ad3ac57a4))
* suppress startup log noise (~350 lines → ~30) ([8a6650e](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/8a6650e57cd8ea396d9b057a7753469947f38d29))
* traceback in Telegram notifications + reduce log padding ([909a403](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/909a4039c43b910761bd05c36e79c8e6773199db))
* use sync context manager for structlog bound_contextvars ([25e8c9f](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/25e8c9f8fc4d2c66d5a1407d3de5c7402dc596da))
### Refactoring
* complete structlog migration with contextvars, kwargs, and logging hardening ([1f0fef1](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/1f0fef114bd979b2b0d2bd38dde6ce05e7bba07b))
## [3.12.1](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/compare/v3.12.0...v3.12.1) (2026-02-16)
+1 -1
View File
@@ -14,7 +14,7 @@ RUN pip install --no-cache-dir --upgrade pip && \
FROM python:3.13-slim
ARG VERSION="v3.15.1" # x-release-please-version
ARG VERSION="v3.12.1" # x-release-please-version
ARG BUILD_DATE
ARG VCS_REF
+5 -5
View File
@@ -1,7 +1,7 @@
"""Email verification token generation and validation."""
import secrets
from datetime import UTC, datetime, timedelta
from datetime import 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.now(UTC) + timedelta(minutes=minutes)
return datetime.utcnow() + 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.now(UTC) + timedelta(hours=hours)
return datetime.utcnow() + 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.now(UTC) + timedelta(hours=hours)
return datetime.utcnow() + 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.now(UTC) > expires_at
return datetime.utcnow() > expires_at
+6 -6
View File
@@ -1,6 +1,6 @@
"""JWT token handling for cabinet authentication."""
from datetime import UTC, datetime, timedelta
from datetime import 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.now(UTC) + timedelta(minutes=expire_minutes)
expires = datetime.utcnow() + timedelta(minutes=expire_minutes)
payload = {
'sub': str(user_id),
'type': 'access',
'exp': expires,
'iat': datetime.now(UTC),
'iat': datetime.utcnow(),
}
# Добавляем 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.now(UTC) + timedelta(days=expire_days)
expires = datetime.utcnow() + timedelta(days=expire_days)
payload = {
'sub': str(user_id),
'type': 'refresh',
'exp': expires,
'iat': datetime.now(UTC),
'iat': datetime.utcnow(),
}
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.now(UTC) + timedelta(days=expire_days)
return datetime.utcnow() + timedelta(days=expire_days)
+5 -5
View File
@@ -3,7 +3,7 @@
import hashlib
import hmac
import json
from datetime import UTC, datetime
from datetime import 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.fromtimestamp(int(auth_date), tz=UTC)
age = (datetime.now(UTC) - auth_time).total_seconds()
auth_time = datetime.utcfromtimestamp(int(auth_date))
age = (datetime.utcnow() - 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.fromtimestamp(int(auth_date), tz=UTC)
age = (datetime.now(UTC) - auth_time).total_seconds()
auth_time = datetime.utcfromtimestamp(int(auth_date))
age = (datetime.utcnow() - auth_time).total_seconds()
if age > max_age_seconds:
return None
except (ValueError, TypeError, OSError):
+2 -2
View File
@@ -1,6 +1,6 @@
"""Admin routes for broadcasts in cabinet."""
from datetime import UTC, datetime
from datetime import 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.now(UTC)
broadcast.completed_at = datetime.utcnow()
await db.commit()
await db.refresh(broadcast)
+12 -25
View File
@@ -57,14 +57,6 @@ def _get_deep_link(start_parameter: str) -> str:
return f'?start={start_parameter}'
def _get_web_link(start_parameter: str) -> str | None:
"""Generate web link for campaign."""
base_url = (settings.MINIAPP_CUSTOM_URL or '').rstrip('/')
if base_url:
return f'{base_url}/?campaign={start_parameter}'
return None
@router.get('/overview', response_model=CampaignsOverviewResponse)
async def get_overview(
admin: User = Depends(get_current_admin_user),
@@ -213,7 +205,6 @@ async def get_campaign(
created_at=campaign.created_at,
updated_at=campaign.updated_at,
deep_link=_get_deep_link(campaign.start_parameter),
web_link=_get_web_link(campaign.start_parameter),
)
@@ -257,7 +248,6 @@ async def get_campaign_stats(
conversion_rate=stats['conversion_rate'],
trial_conversion_rate=stats['trial_conversion_rate'],
deep_link=_get_deep_link(campaign.start_parameter),
web_link=_get_web_link(campaign.start_parameter),
)
@@ -298,22 +288,19 @@ async def get_campaign_registrations(
)
total = count_result.scalar() or 0
# Batch query: find which users have active subscriptions (avoids N+1)
user_ids = [user.id for _reg, user in rows]
active_sub_user_ids: set[int] = set()
if user_ids:
sub_result = await db.execute(
select(Subscription.user_id)
.where(
Subscription.user_id.in_(user_ids),
Subscription.status == 'active',
)
.distinct()
)
active_sub_user_ids = set(sub_result.scalars().all())
items = []
for reg, user in rows:
# Check if user has subscription
sub_result = await db.execute(
select(Subscription)
.where(
Subscription.user_id == user.id,
Subscription.status == 'active',
)
.limit(1)
)
has_sub = sub_result.scalar_one_or_none() is not None
items.append(
CampaignRegistrationItem(
id=reg.id,
@@ -328,7 +315,7 @@ async def get_campaign_registrations(
tariff_duration_days=reg.tariff_duration_days,
created_at=reg.created_at,
user_balance_kopeks=user.balance_kopeks or 0,
has_subscription=user.id in active_sub_user_ids,
has_subscription=has_sub,
has_paid=user.has_had_paid_subscription or False,
)
)
+5 -5
View File
@@ -1,7 +1,7 @@
"""Admin routes for pinned messages in cabinet."""
import time
from datetime import UTC, datetime
from datetime import 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.now(UTC)
msg.updated_at = datetime.utcnow()
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.now(UTC)
msg.updated_at = datetime.utcnow()
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.now(UTC))
.values(is_active=False, updated_at=datetime.utcnow())
)
msg.is_active = True
msg.updated_at = datetime.now(UTC)
msg.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(msg)
+2 -2
View File
@@ -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)
return value.astimezone(UTC).replace(tzinfo=None)
if value.tzinfo is not None:
return value
return value.replace(tzinfo=None)
return value
+3 -6
View File
@@ -1,6 +1,6 @@
"""Admin routes for RemnaWave management in cabinet."""
from datetime import UTC, datetime, timedelta
from datetime import datetime, timedelta
from typing import Any
import structlog
@@ -109,10 +109,7 @@ def _parse_datetime(value: Any) -> datetime | None:
return value
if isinstance(value, str):
try:
parsed = datetime.fromisoformat(value)
if parsed.tzinfo is None:
return parsed.replace(tzinfo=UTC)
return parsed
return datetime.fromisoformat(value)
except ValueError:
return None
return None
@@ -341,7 +338,7 @@ async def get_node_usage(
service = _get_service()
_ensure_configured(service)
end_dt = end or datetime.now(UTC)
end_dt = end or datetime.utcnow()
start_dt = start or (end_dt - timedelta(days=7))
if start_dt >= end_dt:
+5 -5
View File
@@ -2,7 +2,7 @@
import sys
import time
from datetime import UTC, datetime, timedelta
from datetime import 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.now(UTC)
now = datetime.utcnow()
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.now(UTC)
now = datetime.utcnow()
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.now(UTC)
now = datetime.utcnow()
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.now(UTC)
now = datetime.utcnow()
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
week_ago = now - timedelta(days=7)
+1 -1
View File
@@ -535,7 +535,7 @@ async def get_tariff_stats(
# Calculate revenue from subscription payments for users on this tariff
revenue_result = await db.execute(
select(func.coalesce(func.sum(func.abs(Transaction.amount_kopeks)), 0))
select(func.coalesce(func.sum(Transaction.amount_kopeks), 0))
.join(Subscription, Transaction.user_id == Subscription.user_id)
.where(
Subscription.tariff_id == tariff_id,
+7 -7
View File
@@ -1,7 +1,7 @@
"""Admin tickets routes for cabinet."""
import math
from datetime import UTC, datetime
from datetime import datetime
import structlog
from fastapi import APIRouter, Depends, HTTPException, Query, status
@@ -269,7 +269,7 @@ async def update_ticket_settings(
if request.sla_reminder_cooldown_minutes is not None:
settings.SUPPORT_TICKET_SLA_REMINDER_COOLDOWN_MINUTES = request.sla_reminder_cooldown_minutes
if request.support_system_mode is not None:
SupportSettingsService.set_system_mode(request.support_system_mode.strip().lower())
settings.SUPPORT_SYSTEM_MODE = request.support_system_mode.strip().lower()
# Update cabinet notification settings
if request.cabinet_user_notifications_enabled is not None:
@@ -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.now(UTC),
created_at=datetime.utcnow(),
)
db.add(message)
# Update ticket status to answered
ticket.status = 'answered'
ticket.updated_at = datetime.now(UTC)
ticket.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(message)
@@ -522,9 +522,9 @@ async def update_ticket_status(
)
ticket.status = request.status
ticket.updated_at = datetime.now(UTC)
ticket.updated_at = datetime.utcnow()
if request.status == 'closed':
ticket.closed_at = datetime.now(UTC)
ticket.closed_at = datetime.utcnow()
else:
ticket.closed_at = None
@@ -581,7 +581,7 @@ async def update_ticket_priority(
)
ticket.priority = request.priority
ticket.updated_at = datetime.now(UTC)
ticket.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(ticket)
+1 -1
View File
@@ -383,7 +383,7 @@ async def _get_bulk_spending(db: AsyncSession, user_ids: list[int]) -> dict[int,
if not user_ids:
return {}
result = await db.execute(
select(Transaction.user_id, func.coalesce(func.sum(func.abs(Transaction.amount_kopeks)), 0))
select(Transaction.user_id, func.coalesce(func.sum(Transaction.amount_kopeks), 0))
.where(
and_(
Transaction.user_id.in_(user_ids),
+3 -3
View File
@@ -1,6 +1,6 @@
"""Admin routes for version and release information."""
from datetime import UTC, datetime, timedelta
from datetime import 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(UTC) - _cabinet_last_check < timedelta(seconds=_CACHE_TTL):
if datetime.now() - _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(UTC)
_cabinet_last_check = datetime.now()
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)
+52 -43
View File
@@ -1,6 +1,6 @@
"""Admin routes for managing users in cabinet."""
from datetime import UTC, datetime, timedelta
from datetime import 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_utc
from app.utils.timezone import panel_datetime_to_naive_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.now(UTC)
delta = subscription.end_date - datetime.utcnow()
days_remaining = max(0, delta.days)
is_active = subscription.status == SubscriptionStatus.ACTIVE.value and subscription.end_date > datetime.now(UTC)
is_active = subscription.status == SubscriptionStatus.ACTIVE.value and subscription.end_date > datetime.utcnow()
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.now(UTC)
now = datetime.utcnow()
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.now(UTC)
and subscription.end_date > datetime.utcnow()
)
panel_status = PanelUserStatus.ACTIVE if is_active else PanelUserStatus.DISABLED
expire_at = subscription.end_date
if expire_at and expire_at <= datetime.now(UTC):
expire_at = datetime.now(UTC) + timedelta(minutes=1)
if expire_at and expire_at <= datetime.utcnow():
expire_at = datetime.utcnow() + 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.now(UTC) + timedelta(days=30)),
'expire_at': expire_at or (datetime.utcnow() + 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.now(UTC)
user.last_remnawave_sync = datetime.utcnow()
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.now(UTC),
Subscription.end_date > datetime.utcnow(),
),
Integer,
)
@@ -431,7 +431,7 @@ async def get_users_stats(
func.cast(
or_(
Subscription.status == SubscriptionStatus.EXPIRED.value,
Subscription.end_date <= datetime.now(UTC),
Subscription.end_date <= datetime.utcnow(),
),
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.now(UTC)
now = datetime.utcnow()
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.now(UTC)
end_date = datetime.utcnow()
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.now(UTC):
if request.end_date > datetime.utcnow():
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.now(UTC)
subscription.end_date = datetime.utcnow()
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.now(UTC):
if subscription.end_date and subscription.end_date <= datetime.utcnow():
# Extend by 30 days if expired
subscription.end_date = datetime.now(UTC) + timedelta(days=30)
subscription.end_date = datetime.utcnow() + 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.now(UTC)
now = datetime.utcnow()
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.now(UTC)
user.updated_at = datetime.utcnow()
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.now(UTC)
user.updated_at = datetime.utcnow()
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.now(UTC)
user.updated_at = datetime.utcnow()
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.now(UTC)
user.updated_at = datetime.utcnow()
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.now(UTC)
user.updated_at = datetime.utcnow()
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.now(UTC)
user.updated_at = datetime.utcnow()
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.now(UTC)
user.updated_at = datetime.utcnow()
await db.commit()
reason_text = f' (reason: {request.reason})' if request.reason else ''
@@ -2179,8 +2179,13 @@ 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:
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)
# 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)
diff_seconds = abs((bot_end_utc - panel_end_utc).total_seconds())
# Allow for timezone offset (3 hours = MSK) and small sync delays
@@ -2314,7 +2319,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_utc(panel_user.expire_at) if panel_user.expire_at else None,
expire_at=panel_datetime_to_naive_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,
@@ -2333,10 +2338,13 @@ async def sync_user_from_panel(
# Update end date (normalize timezone)
if panel_user.expire_at:
panel_expire_utc = panel_datetime_to_utc(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)
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:
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:
changes['end_date'] = {
'old': sub.end_date.isoformat() if sub.end_date else None,
'new': panel_expire_utc.isoformat(),
@@ -2345,7 +2353,7 @@ async def sync_user_from_panel(
# Update status
panel_status_str = panel_user.status.value if panel_user.status else 'DISABLED'
now = datetime.now(UTC)
now = datetime.utcnow()
# 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:
@@ -2402,8 +2410,9 @@ 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_expire_utc = panel_datetime_to_utc(panel_user.expire_at)
days_remaining = max(1, (panel_expire_utc - datetime.now(UTC)).days)
# 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)
new_sub = await create_paid_subscription(
db=db,
@@ -2418,8 +2427,8 @@ async def sync_user_from_panel(
changes['subscription_created'] = True
# Update last sync time
user.last_remnawave_sync = datetime.now(UTC)
user.updated_at = datetime.now(UTC)
user.last_remnawave_sync = datetime.utcnow()
user.updated_at = datetime.utcnow()
await db.commit()
@@ -2493,14 +2502,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.now(UTC)
and sub.end_date > datetime.utcnow()
)
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.now(UTC):
expire_at = datetime.now(UTC) + timedelta(minutes=1)
if expire_at and expire_at <= datetime.utcnow():
expire_at = datetime.utcnow() + timedelta(minutes=1)
username = settings.format_remnawave_username(
full_name=user.full_name,
@@ -2586,7 +2595,7 @@ async def sync_user_to_panel(
# Create new user in panel
create_kwargs = {
'username': username,
'expire_at': expire_at or (datetime.now(UTC) + timedelta(days=30)),
'expire_at': expire_at or (datetime.utcnow() + timedelta(days=30)),
'status': panel_status,
'traffic_limit_bytes': traffic_limit_bytes,
'traffic_limit_strategy': TrafficLimitStrategy.MONTH,
@@ -2611,8 +2620,8 @@ async def sync_user_to_panel(
action = 'created'
# Update last sync time
user.last_remnawave_sync = datetime.now(UTC)
user.updated_at = datetime.now(UTC)
user.last_remnawave_sync = datetime.utcnow()
user.updated_at = datetime.utcnow()
await db.commit()
+20 -87
View File
@@ -11,10 +11,6 @@ from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database.crud.campaign import (
get_campaign_by_start_parameter,
get_campaign_registration_by_user,
)
from app.database.crud.user import (
clear_email_change_pending,
create_user,
@@ -27,10 +23,9 @@ from app.database.crud.user import (
verify_and_apply_email_change,
)
from app.database.models import CabinetRefreshToken, User
from app.services.campaign_service import AdvertisingCampaignService
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_utc
from app.utils.timezone import panel_datetime_to_naive_utc
from ..auth import (
create_access_token,
@@ -54,7 +49,6 @@ from ..auth.jwt_handler import get_refresh_token_expires_at
from ..dependencies import get_cabinet_db, get_current_cabinet_user
from ..schemas.auth import (
AuthResponse,
CampaignBonusInfo,
EmailChangeRequest,
EmailChangeResponse,
EmailChangeVerifyRequest,
@@ -124,6 +118,12 @@ async def _store_refresh_token(
token_hash = hashlib.sha256(refresh_token.encode()).hexdigest()
expires_at = get_refresh_token_expires_at()
# Check if token already exists (handles race conditions)
existing = await db.execute(select(CabinetRefreshToken).where(CabinetRefreshToken.token_hash == token_hash))
if existing.scalar_one_or_none():
# Token already stored, skip
return
token_record = CabinetRefreshToken(
user_id=user_id,
token_hash=token_hash,
@@ -133,56 +133,9 @@ async def _store_refresh_token(
db.add(token_record)
try:
await db.commit()
except IntegrityError:
await db.rollback()
logger.debug('Refresh token already exists (duplicate)', user_id=user_id)
async def _process_campaign_bonus(
db: AsyncSession,
user: User,
campaign_slug: str | None,
) -> CampaignBonusInfo | None:
"""Process campaign bonus for user during auth. Never raises."""
if not campaign_slug:
return None
try:
campaign = await get_campaign_by_start_parameter(db, campaign_slug, only_active=True)
if not campaign:
return None
# Lock user row to prevent concurrent bonus application (race condition)
await db.execute(select(User).where(User.id == user.id).with_for_update())
existing = await get_campaign_registration_by_user(db, user.id)
if existing:
logger.debug('User already has campaign registration', user_id=user.id)
return None
service = AdvertisingCampaignService()
result = await service.apply_campaign_bonus(db, user, campaign)
if not result.success:
return None
# Refresh user to get updated balance after bonus
await db.refresh(user)
return CampaignBonusInfo(
campaign_name=campaign.name,
bonus_type=result.bonus_type or campaign.bonus_type,
balance_kopeks=result.balance_kopeks,
subscription_days=result.subscription_days,
tariff_name=result.tariff_name,
)
except Exception:
logger.exception('Failed to process campaign bonus', user_id=user.id, campaign_slug=campaign_slug)
try:
await db.rollback()
# Re-fetch user so session stays usable for the caller
await db.refresh(user)
except Exception:
logger.exception('Failed to rollback after campaign bonus error', user_id=user.id)
return None
# Handle race condition if token was inserted between check and insert
await db.rollback()
async def _sync_subscription_from_panel_by_email(db: AsyncSession, user: User) -> None:
@@ -222,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_utc(panel_user.expire_at)
expire_at = panel_datetime_to_naive_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
@@ -233,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)
current_time = datetime.now(UTC).replace(tzinfo=None)
if panel_user.status.value == 'ACTIVE' and expire_at > current_time:
sub_status = SubscriptionStatus.ACTIVE
@@ -360,7 +313,7 @@ async def auth_telegram(
)
# Update last login
user.cabinet_last_login = datetime.now(UTC)
user.cabinet_last_login = datetime.utcnow()
await db.commit()
response = _create_auth_response(user)
@@ -368,11 +321,6 @@ async def auth_telegram(
# Store refresh token
await _store_refresh_token(db, user.id, response.refresh_token)
# Process campaign bonus
response.campaign_bonus = await _process_campaign_bonus(db, user, request.campaign_slug)
if response.campaign_bonus:
response.user = _user_to_response(user)
return response
@@ -387,7 +335,7 @@ async def auth_telegram_widget(
This endpoint validates data from Telegram Login Widget and returns
JWT tokens for authenticated access.
"""
widget_data = request.model_dump(exclude={'campaign_slug'})
widget_data = request.model_dump()
if not validate_telegram_login_widget(widget_data):
raise HTTPException(
@@ -426,17 +374,12 @@ async def auth_telegram_widget(
if request.last_name != user.last_name:
user.last_name = request.last_name
user.cabinet_last_login = datetime.now(UTC)
user.cabinet_last_login = datetime.utcnow()
await db.commit()
response = _create_auth_response(user)
await _store_refresh_token(db, user.id, response.refresh_token)
# Process campaign bonus
response.campaign_bonus = await _process_campaign_bonus(db, user, request.campaign_slug)
if response.campaign_bonus:
response.user = _user_to_response(user)
return response
@@ -603,7 +546,7 @@ async def register_email_standalone(
# Для тестового email - автоматически верифицировать
if is_test_email:
user.email_verified = True
user.email_verified_at = datetime.now(UTC)
user.email_verified_at = datetime.utcnow()
await db.commit()
logger.info('Test email auto-verified: user_id', email=request.email, user_id=user.id)
else:
@@ -690,10 +633,10 @@ async def verify_email(
# Mark email as verified
user.email_verified = True
user.email_verified_at = datetime.now(UTC)
user.email_verified_at = datetime.utcnow()
user.email_verification_token = None
user.email_verification_expires = None
user.cabinet_last_login = datetime.now(UTC)
user.cabinet_last_login = datetime.utcnow()
await db.commit()
@@ -704,11 +647,6 @@ async def verify_email(
response = _create_auth_response(user)
await _store_refresh_token(db, user.id, response.refresh_token)
# Process campaign bonus
response.campaign_bonus = await _process_campaign_bonus(db, user, request.campaign_slug)
if response.campaign_bonus:
response.user = _user_to_response(user)
return response
@@ -812,7 +750,7 @@ async def login_email(
language='ru',
)
user.email_verified = True
user.email_verified_at = datetime.now(UTC)
user.email_verified_at = datetime.utcnow()
await db.commit()
else:
raise HTTPException(
@@ -845,17 +783,12 @@ async def login_email(
detail='User account is not active',
)
user.cabinet_last_login = datetime.now(UTC)
user.cabinet_last_login = datetime.utcnow()
await db.commit()
response = _create_auth_response(user)
await _store_refresh_token(db, user.id, response.refresh_token)
# Process campaign bonus
response.campaign_bonus = await _process_campaign_bonus(db, user, request.campaign_slug)
if response.campaign_bonus:
response.user = _user_to_response(user)
return response
@@ -938,7 +871,7 @@ async def logout(
token_record = result.scalar_one_or_none()
if token_record:
token_record.revoked_at = datetime.now(UTC)
token_record.revoked_at = datetime.utcnow()
await db.commit()
return {'message': 'Logged out successfully'}
+2 -2
View File
@@ -1,7 +1,7 @@
"""Contests routes for cabinet - user participation in games/contests."""
import random
from datetime import UTC, datetime, timedelta
from datetime import 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.now(UTC)
subscription.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(subscription)
+2 -2
View File
@@ -1,6 +1,6 @@
"""Notification settings routes for cabinet."""
from datetime import UTC, datetime
from datetime import 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.now(UTC)
user.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(user)
+6 -19
View File
@@ -24,7 +24,7 @@ from ..auth.oauth_providers import (
)
from ..dependencies import get_cabinet_db
from ..schemas.auth import AuthResponse
from .auth import _create_auth_response, _process_campaign_bonus, _store_refresh_token
from .auth import _create_auth_response, _store_refresh_token
logger = structlog.get_logger(__name__)
@@ -32,22 +32,12 @@ logger = structlog.get_logger(__name__)
router = APIRouter(prefix='/auth/oauth', tags=['Cabinet OAuth'])
async def _finalize_oauth_login(
db: AsyncSession,
user: User,
provider: str,
campaign_slug: str | None = None,
) -> AuthResponse:
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)
user.cabinet_last_login = datetime.now(UTC).replace(tzinfo=None)
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}')
auth_response.campaign_bonus = await _process_campaign_bonus(db, user, campaign_slug)
if auth_response.campaign_bonus:
from .auth import _user_to_response
auth_response.user = _user_to_response(user)
return auth_response
@@ -71,9 +61,6 @@ class OAuthAuthorizeResponse(BaseModel):
class OAuthCallbackRequest(BaseModel):
code: str = Field(..., description='Authorization code from provider')
state: str = Field(..., description='CSRF state token')
campaign_slug: str | None = Field(
None, min_length=1, max_length=64, pattern=r'^[a-zA-Z0-9_-]+$', description='Campaign slug from web link'
)
# --- Endpoints ---
@@ -153,7 +140,7 @@ async def oauth_callback(
user = await get_user_by_oauth_provider(db, provider, user_info.provider_id)
if user:
logger.info('OAuth login via for existing user', provider=provider, user_id=user.id)
return await _finalize_oauth_login(db, user, provider, request.campaign_slug)
return await _finalize_oauth_login(db, user, provider)
# 6. Find user by email (if verified) and link provider
if user_info.email and user_info.email_verified:
@@ -161,7 +148,7 @@ async def oauth_callback(
if user:
await set_user_oauth_provider_id(db, user, provider, user_info.provider_id)
logger.info('OAuth login via linked to existing email user', provider=provider, user_id=user.id)
return await _finalize_oauth_login(db, user, provider, request.campaign_slug)
return await _finalize_oauth_login(db, user, provider)
# 7. Create new user
user = await create_user_by_oauth(
@@ -175,4 +162,4 @@ async def oauth_callback(
username=user_info.username,
)
logger.info('OAuth new user created via with id', provider=provider, user_id=user.id)
return await _finalize_oauth_login(db, user, provider, request.campaign_slug)
return await _finalize_oauth_login(db, user, provider)
+3 -3
View File
@@ -1,6 +1,6 @@
"""Polls routes for cabinet - user participation in polls/surveys."""
from datetime import UTC, datetime
from datetime import 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.now(UTC)
response.started_at = datetime.utcnow()
await db.commit()
# Get next unanswered question
@@ -346,7 +346,7 @@ async def answer_question(
)
# Poll completed
response.completed_at = datetime.now(UTC)
response.completed_at = datetime.utcnow()
await db.commit()
# Award reward if any
+5 -5
View File
@@ -1,6 +1,6 @@
"""Promo offers routes for cabinet - personal discounts and offers."""
from datetime import UTC, datetime, timedelta
from datetime import 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.now(UTC)
now = datetime.utcnow()
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.now(UTC)
now = datetime.utcnow()
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.now(UTC)
now = datetime.utcnow()
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.now(UTC)
user.updated_at = datetime.utcnow()
await db.commit()
+30 -30
View File
@@ -135,7 +135,7 @@ def _subscription_to_response(
traffic_purchases: list[dict[str, Any]] | None = None,
) -> SubscriptionData:
"""Convert Subscription model to response."""
now = datetime.now(UTC)
now = datetime.utcnow()
# 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.now(UTC)
now = datetime.utcnow()
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.now(UTC)
now = datetime.utcnow()
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 UTC, timedelta
from datetime import timedelta
subscription.traffic_reset_at = datetime.now(UTC) + timedelta(days=30)
subscription.traffic_reset_at = datetime.utcnow() + 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.now(UTC)
now = datetime.utcnow()
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.now(UTC)
now = datetime.utcnow()
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.now(UTC)
subscription.last_daily_charge_at = datetime.utcnow()
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.now(UTC) - subscription.start_date).total_seconds() < 60
subscription.start_date and (datetime.utcnow() - 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.now(UTC) - subscription.start_date).total_seconds() < 60
subscription.start_date and (datetime.utcnow() - 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 UTC, datetime
from datetime import 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 UTC, datetime
from datetime import 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 UTC, datetime
from datetime import datetime
delta = user.subscription.end_date - datetime.now(UTC)
delta = user.subscription.end_date - datetime.utcnow()
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.now(UTC)
user.subscription.updated_at = datetime.utcnow()
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.now(UTC)
subscription.updated_at = datetime.utcnow()
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.now(UTC):
delta = user.subscription.end_date - datetime.now(UTC)
if user.subscription.end_date and user.subscription.end_date > datetime.utcnow():
delta = user.subscription.end_date - datetime.utcnow()
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 UTC, timedelta
from datetime import 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.now(UTC):
delta = user.subscription.end_date - datetime.now(UTC)
if user.subscription.end_date and user.subscription.end_date > datetime.utcnow():
delta = user.subscription.end_date - datetime.utcnow()
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.now(UTC) + timedelta(days=1)
user.subscription.last_daily_charge_at = datetime.now(UTC)
user.subscription.end_date = datetime.utcnow() + timedelta(days=1)
user.subscription.last_daily_charge_at = datetime.utcnow()
user.subscription.is_daily_paused = False
elif switching_from_daily:
user.subscription.end_date = datetime.now(UTC) + timedelta(days=new_period_days)
user.subscription.end_date = datetime.utcnow() + timedelta(days=new_period_days)
user.subscription.is_daily_paused = False
user.subscription.updated_at = datetime.now(UTC)
user.subscription.updated_at = datetime.utcnow()
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 UTC, timedelta
from datetime import 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.now(UTC)
user.subscription.end_date = datetime.now(UTC) + timedelta(days=1)
user.subscription.last_daily_charge_at = datetime.utcnow()
user.subscription.end_date = datetime.utcnow() + 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.now(UTC)
user.subscription.updated_at = datetime.utcnow()
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.now(UTC)
user.subscription.updated_at = datetime.utcnow()
await db.commit()
# Calculate percentage
+6 -6
View File
@@ -1,7 +1,7 @@
"""Support tickets routes for cabinet."""
import math
from datetime import UTC, datetime
from datetime import 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.now(UTC),
updated_at=datetime.now(UTC),
created_at=datetime.utcnow(),
updated_at=datetime.utcnow(),
)
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.now(UTC),
created_at=datetime.utcnow(),
)
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.now(UTC),
created_at=datetime.utcnow(),
)
db.add(message)
# Update ticket status and timestamp
if ticket.status == 'answered':
ticket.status = 'pending'
ticket.updated_at = datetime.now(UTC)
ticket.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(message)
-23
View File
@@ -9,9 +9,6 @@ class TelegramAuthRequest(BaseModel):
"""Request for Telegram WebApp initData authentication."""
init_data: str = Field(..., description='Telegram WebApp initData string')
campaign_slug: str | None = Field(
None, min_length=1, max_length=64, pattern=r'^[a-zA-Z0-9_-]+$', description='Campaign slug from web link'
)
class TelegramWidgetAuthRequest(BaseModel):
@@ -24,9 +21,6 @@ class TelegramWidgetAuthRequest(BaseModel):
photo_url: str | None = Field(None, description="User's photo URL")
auth_date: int = Field(..., description='Unix timestamp of authentication')
hash: str = Field(..., description='Authentication hash')
campaign_slug: str | None = Field(
None, min_length=1, max_length=64, pattern=r'^[a-zA-Z0-9_-]+$', description='Campaign slug from web link'
)
class EmailRegisterRequest(BaseModel):
@@ -40,9 +34,6 @@ class EmailVerifyRequest(BaseModel):
"""Request to verify email with token."""
token: str = Field(..., description='Email verification token')
campaign_slug: str | None = Field(
None, min_length=1, max_length=64, pattern=r'^[a-zA-Z0-9_-]+$', description='Campaign slug from web link'
)
class EmailLoginRequest(BaseModel):
@@ -50,9 +41,6 @@ class EmailLoginRequest(BaseModel):
email: EmailStr = Field(..., description='Email address')
password: str = Field(..., description='Password')
campaign_slug: str | None = Field(
None, min_length=1, max_length=64, pattern=r'^[a-zA-Z0-9_-]+$', description='Campaign slug from web link'
)
class RefreshTokenRequest(BaseModel):
@@ -114,16 +102,6 @@ class EmailRegisterStandaloneRequest(BaseModel):
referral_code: str | None = Field(None, max_length=32, description='Referral code of inviter')
class CampaignBonusInfo(BaseModel):
"""Info about campaign bonus applied during auth."""
campaign_name: str
bonus_type: str
balance_kopeks: int = 0
subscription_days: int | None = None
tariff_name: str | None = None
class AuthResponse(BaseModel):
"""Full authentication response with tokens and user."""
@@ -132,7 +110,6 @@ class AuthResponse(BaseModel):
token_type: str = 'bearer'
expires_in: int
user: UserResponse
campaign_bonus: CampaignBonusInfo | None = None
class RegisterResponse(BaseModel):
+2 -4
View File
@@ -66,7 +66,6 @@ class CampaignDetailResponse(BaseModel):
updated_at: datetime | None = None
# Deep link
deep_link: str | None = None
web_link: str | None = None
class Config:
from_attributes = True
@@ -76,7 +75,7 @@ class CampaignCreateRequest(BaseModel):
"""Request to create a campaign."""
name: str = Field(..., min_length=1, max_length=255)
start_parameter: str = Field(..., min_length=1, max_length=64, pattern=r'^[a-zA-Z0-9_-]+$')
start_parameter: str = Field(..., min_length=1, max_length=100, pattern=r'^[a-zA-Z0-9_-]+$')
bonus_type: CampaignBonusType
is_active: bool = True
# Balance bonus
@@ -95,7 +94,7 @@ class CampaignUpdateRequest(BaseModel):
"""Request to update a campaign."""
name: str | None = Field(None, min_length=1, max_length=255)
start_parameter: str | None = Field(None, min_length=1, max_length=64, pattern=r'^[a-zA-Z0-9_-]+$')
start_parameter: str | None = Field(None, min_length=1, max_length=100, pattern=r'^[a-zA-Z0-9_-]+$')
bonus_type: CampaignBonusType | None = None
is_active: bool | None = None
# Balance bonus
@@ -148,7 +147,6 @@ class CampaignStatisticsResponse(BaseModel):
trial_conversion_rate: float = 0.0
# Deep link
deep_link: str | None = None
web_link: str | None = None
class CampaignRegistrationItem(BaseModel):
@@ -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 UTC, datetime
from datetime import datetime
from typing import Any
import structlog
@@ -124,7 +124,7 @@ async def save_template_override(
)
row = existing.fetchone()
now = datetime.now(UTC)
now = datetime.utcnow()
if row:
# Update
-1
View File
@@ -549,7 +549,6 @@ class Settings(BaseSettings):
LOG_LEVEL: str = 'INFO'
LOG_FILE: str = 'logs/bot.log'
LOG_COLORS: bool = True # ANSI-цвета в консоли (false для plain-text вывода)
# === Log Rotation Settings ===
LOG_ROTATION_ENABLED: bool = False # По умолчанию старое поведение
+63 -3
View File
@@ -1,4 +1,4 @@
from datetime import UTC, datetime
from datetime import 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.now(UTC)
update_data['updated_at'] = datetime.utcnow()
await db.execute(update(AdvertisingCampaign).where(AdvertisingCampaign.id == campaign.id).values(**update_data))
await db.commit()
@@ -331,7 +331,7 @@ async def get_campaign_statistics(
first_payment_time_by_user[user_id] = converted_at
for user_id, amount_kopeks, created_at in subscription_payments:
amount_value = abs(int(amount_kopeks or 0))
amount_value = int(amount_kopeks or 0)
subscription_payments_total += amount_value
paid_users_from_transactions.add(user_id)
@@ -359,6 +359,66 @@ async def get_campaign_statistics(
if first_payment_amount_by_user:
avg_first_payment = int(sum(first_payment_amount_by_user.values()) / len(first_payment_amount_by_user))
conversion_rate = 0.0
if count:
conversion_rate = round((paid_users_count / count) * 100, 1)
trial_conversion_rate = 0.0
if trial_users_count:
trial_conversion_rate = round((conversion_count / trial_users_count) * 100, 1)
avg_revenue_per_user = 0
if count:
avg_revenue_per_user = int(total_revenue / count)
deposits_result = await db.execute(
select(func.coalesce(func.sum(Transaction.amount_kopeks), 0)).where(
Transaction.user_id.in_(select(registrations_subquery.c.user_id)),
Transaction.type == TransactionType.DEPOSIT.value,
Transaction.is_completed.is_(True),
)
)
total_revenue = deposits_result.scalar() or 0
trials_result = await db.execute(
select(func.count(func.distinct(Subscription.user_id))).where(
Subscription.user_id.in_(select(registrations_subquery.c.user_id)),
Subscription.is_trial.is_(True),
)
)
trial_users_count = trials_result.scalar() or 0
active_trials_result = await db.execute(
select(func.count(func.distinct(Subscription.user_id))).where(
Subscription.user_id.in_(select(registrations_subquery.c.user_id)),
Subscription.is_trial.is_(True),
Subscription.status == SubscriptionStatus.ACTIVE.value,
)
)
active_trials_count = active_trials_result.scalar() or 0
conversions_result = await db.execute(
select(func.count(func.distinct(SubscriptionConversion.user_id))).where(
SubscriptionConversion.user_id.in_(select(registrations_subquery.c.user_id))
)
)
conversion_count = conversions_result.scalar() or 0
paid_users_result = await db.execute(
select(func.count(User.id)).where(
User.id.in_(select(registrations_subquery.c.user_id)),
User.has_had_paid_subscription.is_(True),
)
)
paid_users_count = paid_users_result.scalar() or 0
avg_first_payment_result = await db.execute(
select(func.coalesce(func.avg(SubscriptionConversion.first_payment_amount_kopeks), 0)).where(
SubscriptionConversion.user_id.in_(select(registrations_subquery.c.user_id))
)
)
avg_first_payment = int(avg_first_payment_result.scalar() or 0)
conversion_rate = 0.0
if count:
conversion_rate = round((paid_users_count / count) * 100, 1)
+4 -4
View File
@@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import UTC, datetime
from datetime import 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.now(UTC)
payment.updated_at = datetime.utcnow()
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.now(UTC)
payment.paid_at = datetime.utcnow()
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.now(UTC)
payment.updated_at = datetime.utcnow()
await db.flush()
await db.refresh(payment)
+3 -3
View File
@@ -1,5 +1,5 @@
from collections.abc import Sequence
from datetime import UTC, datetime
from datetime import 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.now(UTC)
now = datetime.utcnow()
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.now(UTC)
now = datetime.utcnow()
result = await db.execute(
select(ContestRound)
.options(selectinload(ContestRound.template))
+5 -5
View File
@@ -1,4 +1,4 @@
from datetime import UTC, datetime
from datetime import 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.now(UTC)
payment.updated_at = datetime.utcnow()
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.now(UTC)
payment.updated_at = datetime.utcnow()
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 UTC, timedelta
from datetime import timedelta
cutoff_time = datetime.now(UTC) - timedelta(hours=older_than_hours)
cutoff_time = datetime.utcnow() - timedelta(hours=older_than_hours)
result = await db.execute(
select(CryptoBotPayment)
+5 -5
View File
@@ -1,6 +1,6 @@
from __future__ import annotations
from datetime import UTC, datetime, timedelta
from datetime import 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.now(UTC) + timedelta(hours=valid_hours)
expires_at = datetime.utcnow() + 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.now(UTC)
now = datetime.utcnow()
stmt = (
select(DiscountOffer)
.options(
@@ -161,7 +161,7 @@ async def mark_offer_claimed(
*,
details: dict | None = None,
) -> DiscountOffer:
offer.claimed_at = datetime.now(UTC)
offer.claimed_at = datetime.utcnow()
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.now(UTC)
now = datetime.utcnow()
result = await db.execute(
select(DiscountOffer).where(
DiscountOffer.is_active == True,
+4 -4
View File
@@ -1,5 +1,5 @@
from collections.abc import Iterable
from datetime import UTC, datetime
from datetime import 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.now(UTC)
setting.updated_at = datetime.utcnow()
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.now(UTC)
page.updated_at = datetime.utcnow()
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.now(UTC))
update(FaqPage).where(FaqPage.id == page_id).values(display_order=order, updated_at=datetime.utcnow())
)
await db.commit()
+4 -4
View File
@@ -1,7 +1,7 @@
"""CRUD операции для платежей Freekassa."""
import json
from datetime import UTC, datetime
from datetime import 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.now(UTC)
payment.updated_at = datetime.utcnow()
if is_paid:
payment.paid_at = datetime.now(UTC)
payment.paid_at = datetime.utcnow()
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.now(UTC)
now = datetime.utcnow()
result = await db.execute(
select(FreekassaPayment).where(
FreekassaPayment.status == 'pending',
+3 -3
View File
@@ -1,4 +1,4 @@
from datetime import UTC, datetime
from datetime import 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.now(UTC)
payment.updated_at = datetime.utcnow()
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.now(UTC)
payment.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(payment)
+4 -4
View File
@@ -1,7 +1,7 @@
"""CRUD операции для платежей KassaAI."""
import json
from datetime import UTC, datetime
from datetime import 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.now(UTC)
payment.updated_at = datetime.utcnow()
if is_paid:
payment.paid_at = datetime.now(UTC)
payment.paid_at = datetime.utcnow()
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.now(UTC)
now = datetime.utcnow()
result = await db.execute(
select(KassaAiPayment).where(
KassaAiPayment.status == 'pending',
+4 -4
View File
@@ -1,4 +1,4 @@
from datetime import UTC, datetime
from datetime import 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.now(UTC)
payment.updated_at = datetime.utcnow()
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.now(UTC)
payment.updated_at = datetime.utcnow()
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.now(UTC)
payment.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(payment)
return payment
+3 -3
View File
@@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import UTC, datetime
from datetime import 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.now(UTC)
payment.updated_at = datetime.utcnow()
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.now(UTC)
payment.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(payment)
return payment
+3 -3
View File
@@ -1,4 +1,4 @@
from datetime import UTC, datetime
from datetime import 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.now(UTC)
policy.updated_at = datetime.utcnow()
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.now(UTC)
policy.updated_at = datetime.utcnow()
else:
policy = PrivacyPolicy(
language=language,
+4 -4
View File
@@ -1,7 +1,7 @@
from __future__ import annotations
from collections.abc import Iterable
from datetime import UTC, datetime
from datetime import 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.now(UTC)
existing.updated_at = datetime.utcnow()
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.now(UTC)
existing.updated_at = datetime.utcnow()
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.now(UTC)
template.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(template)
+6 -6
View File
@@ -1,4 +1,4 @@
from datetime import UTC, datetime
from datetime import 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.now(UTC))
promocode_use = PromoCodeUse(promocode_id=promocode_id, user_id=user_id, used_at=datetime.utcnow())
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 UTC, timedelta
from datetime import timedelta
cutoff = datetime.now(UTC) - timedelta(hours=hours)
cutoff = datetime.utcnow() - 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.now(UTC)
promocode.updated_at = datetime.utcnow()
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.now(UTC).date()
today = datetime.utcnow().date()
today_uses_result = await db.execute(
select(func.count(PromoCodeUse.id)).where(
and_(PromoCodeUse.promocode_id == promocode_id, PromoCodeUse.used_at >= today)
+3 -3
View File
@@ -1,4 +1,4 @@
from datetime import UTC, datetime
from datetime import 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.now(UTC)
offer.updated_at = datetime.utcnow()
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.now(UTC)
offer.updated_at = datetime.utcnow()
else:
offer = PublicOffer(
language=language,
+7 -7
View File
@@ -1,4 +1,4 @@
from datetime import UTC, datetime, timedelta
from datetime import datetime, timedelta
import structlog
from sqlalchemy import and_, func, select
@@ -178,7 +178,7 @@ async def get_referral_statistics(db: AsyncSession) -> dict:
}
)
today = datetime.now(UTC).replace(hour=0, minute=0, second=0, microsecond=0)
today = datetime.utcnow().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.now(UTC) - timedelta(days=7)
week_ago = datetime.utcnow() - 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.now(UTC) - timedelta(days=30)
month_ago = datetime.utcnow() - 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.now(UTC)
now = datetime.utcnow()
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.now(UTC) - timedelta(days=30)
month_ago = datetime.utcnow() - 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.now(UTC)
current_time = datetime.utcnow()
active_referrals_result = await db.execute(
select(func.count(User.id))
+8 -8
View File
@@ -1,5 +1,5 @@
from collections.abc import Sequence
from datetime import UTC, date, datetime, time
from datetime import 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.now(UTC),
occurred_at=datetime.utcnow(),
)
db.add(event)
await db.commit()
@@ -440,7 +440,7 @@ async def get_contest_transaction_breakdown(
# Сумма покупок подписок
subscription_result = await db.execute(
select(func.coalesce(func.sum(func.abs(Transaction.amount_kopeks)), 0)).where(
select(func.coalesce(func.sum(Transaction.amount_kopeks), 0)).where(
and_(
Transaction.user_id.in_(referral_ids),
Transaction.is_completed.is_(True),
@@ -512,7 +512,7 @@ async def upsert_contest_event(
referral_id=referral_id,
amount_kopeks=amount_kopeks,
event_type=event_type,
occurred_at=datetime.now(UTC),
occurred_at=datetime.utcnow(),
)
db.add(event)
await db.commit()
@@ -621,10 +621,10 @@ async def debug_contest_transactions(
tx.amount_kopeks for tx in txs_in if tx.type == TransactionType.DEPOSIT.value and tx.payment_method is not None
)
subscription_in_period = sum(
abs(tx.amount_kopeks) for tx in txs_in if tx.type == TransactionType.SUBSCRIPTION_PAYMENT.value
tx.amount_kopeks for tx in txs_in if tx.type == TransactionType.SUBSCRIPTION_PAYMENT.value
)
total_in_period = deposit_in_period + subscription_in_period
total_outside = sum(abs(tx.amount_kopeks) for tx in txs_out)
total_outside = sum(tx.amount_kopeks for tx in txs_out)
# Подсчёт ПОЛНЫХ сумм (не только sample, БЕЗ бонусов)
full_deposit_result = await db.execute(
@@ -642,7 +642,7 @@ async def debug_contest_transactions(
full_deposit_total = int(full_deposit_result.scalar_one() or 0)
full_subscription_result = await db.execute(
select(func.coalesce(func.sum(func.abs(Transaction.amount_kopeks)), 0)).where(
select(func.coalesce(func.sum(Transaction.amount_kopeks), 0)).where(
and_(
Transaction.user_id.in_(referral_ids),
Transaction.is_completed.is_(True),
@@ -773,7 +773,7 @@ async def sync_contest_events(
for event in events:
# Считаем ТОЛЬКО покупки подписок (реальные траты на подписки)
subscription_query = select(func.coalesce(func.sum(func.abs(Transaction.amount_kopeks)), 0)).where(
subscription_query = select(func.coalesce(func.sum(Transaction.amount_kopeks), 0)).where(
and_(
Transaction.user_id == event.referral_id,
Transaction.is_completed.is_(True),
+4 -4
View File
@@ -1,4 +1,4 @@
from datetime import UTC, datetime
from datetime import 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.now(UTC)
rule.updated_at = datetime.utcnow()
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.now(UTC))
.values(is_active=False, updated_at=datetime.utcnow())
)
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.now(UTC))
.values(is_active=False, updated_at=datetime.utcnow())
)
restored_rule = ServiceRule(
+3 -3
View File
@@ -1,6 +1,6 @@
import random
from collections.abc import Iterable, Sequence
from datetime import UTC, datetime
from datetime import 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.now(UTC)
subscription.updated_at = datetime.utcnow()
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.now(UTC)
tariff.updated_at = datetime.utcnow()
cleaned_tariffs += 1
logger.info(
'🧹 Тариф "%s" (ID: %s): удалены несуществующие сквады %s',
+39 -39
View File
@@ -31,7 +31,7 @@ def is_recently_updated_by_webhook(subscription: Subscription) -> bool:
"""Return True if subscription was updated by webhook within guard window."""
if not subscription.last_webhook_update_at:
return False
elapsed = (datetime.now(UTC) - subscription.last_webhook_update_at).total_seconds()
elapsed = (datetime.now(UTC).replace(tzinfo=None) - 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.now(UTC) + timedelta(days=duration_days)
end_date = datetime.utcnow() + 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.now(UTC)
existing.start_date = datetime.utcnow()
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.now(UTC),
start_date=datetime.utcnow(),
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.now(UTC) + timedelta(days=duration_days)
end_date = datetime.utcnow() + 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.now(UTC),
start_date=datetime.utcnow(),
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.now(UTC)
current_time = datetime.utcnow()
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.now(UTC)
current_time = datetime.utcnow()
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.now(UTC)
subscription.updated_at = datetime.utcnow()
# Создаём новую запись докупки с индивидуальной датой истечения (30 дней)
from datetime import UTC, timedelta
from datetime import timedelta
from sqlalchemy import select as sql_select
from app.database.models import TrafficPurchase
new_expires_at = datetime.now(UTC) + timedelta(days=30)
new_expires_at = datetime.utcnow() + 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.now(UTC)
now = datetime.utcnow()
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.now(UTC)
subscription.updated_at = datetime.utcnow()
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.now(UTC)
subscription.updated_at = datetime.utcnow()
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.now(UTC)
subscription.updated_at = datetime.utcnow()
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.now(UTC)
subscription.updated_at = datetime.utcnow()
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.now(UTC)
subscription.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(subscription)
@@ -715,7 +715,7 @@ async def reactivate_subscription(db: AsyncSession, subscription: Subscription)
Активирует только если подписка была DISABLED и ещё не истекла.
Не логирует если реактивация не требуется.
"""
now = datetime.now(UTC)
now = datetime.utcnow()
# Тихо выходим если реактивация не нужна
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.now(UTC) + timedelta(days=days_before)
threshold_date = datetime.utcnow() + 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.now(UTC),
Subscription.end_date > datetime.utcnow(),
)
)
)
@@ -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.now(UTC)))
.where(and_(Subscription.status == SubscriptionStatus.ACTIVE.value, Subscription.end_date <= datetime.utcnow()))
)
return result.scalars().all()
async def get_subscriptions_for_autopay(db: AsyncSession) -> list[Subscription]:
current_time = datetime.now(UTC)
current_time = datetime.utcnow()
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.now(UTC).date()
today = datetime.utcnow().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.now(UTC) - timedelta(days=7)
week_ago = datetime.utcnow() - 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.now(UTC) - timedelta(days=30)
month_ago = datetime.utcnow() - 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.now(UTC)
now = datetime.utcnow()
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.now(UTC)
now = datetime.utcnow()
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.now(UTC)
subscription.updated_at = datetime.utcnow()
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.now(UTC)
subscription.updated_at = datetime.utcnow()
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.now(UTC)
current_time = datetime.utcnow()
logger.info(
'🔍 Проверка статуса подписки , текущий статус дата окончания текущее время',
@@ -1595,7 +1595,7 @@ async def create_subscription_no_commit(
"""
if end_date is None:
end_date = datetime.now(UTC) + timedelta(days=3)
end_date = datetime.utcnow() + 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.now(UTC) + timedelta(days=3)
end_date = datetime.utcnow() + 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.now(UTC)
current_time = datetime.utcnow()
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.now(UTC)
current_time = datetime.utcnow()
pending_subscription.status = SubscriptionStatus.ACTIVE.value
# Если указан период, обновляем дату окончания
@@ -1870,7 +1870,7 @@ async def activate_pending_trial_subscription(
)
# Обновляем статус подписки на ACTIVE
current_time = datetime.now(UTC)
current_time = datetime.utcnow()
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.now(UTC)
now = datetime.utcnow()
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.now(UTC)
subscription.end_date = datetime.now(UTC) + timedelta(days=1)
subscription.last_daily_charge_at = datetime.utcnow()
subscription.end_date = datetime.utcnow() + 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.now(UTC)
now = charge_time or datetime.utcnow()
subscription.last_daily_charge_at = now
# Продлеваем подписку на 1 день от текущего момента
+3 -3
View File
@@ -1,4 +1,4 @@
from datetime import UTC, datetime, timedelta
from datetime import 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.now(UTC),
converted_at=datetime.utcnow(),
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.now(UTC) - timedelta(days=30)
month_ago = datetime.utcnow() - timedelta(days=30)
month_conversions_result = await db.execute(
select(func.count(SubscriptionConversion.id)).where(SubscriptionConversion.converted_at >= month_ago)
)
+2 -2
View File
@@ -1,7 +1,7 @@
from __future__ import annotations
from collections.abc import Iterable
from datetime import UTC, datetime
from datetime import 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.now(UTC),
occurred_at=occurred_at or datetime.utcnow(),
extra=extra or None,
)
db.add(event)
+9 -9
View File
@@ -1,4 +1,4 @@
from datetime import UTC, datetime
from datetime import 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 UTC, datetime
from datetime import datetime
# Если есть вечная блокировка в любом тикете — блок активен без срока
for t in tickets:
if t.user_reply_block_permanent:
return datetime.max.replace(tzinfo=UTC)
return datetime.max
# Иначе ищем максимальный срок блокировки, если он в будущем
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.now(UTC) else None
return max_until if max_until > datetime.utcnow() else None
@staticmethod
async def get_all_tickets(
@@ -225,7 +225,7 @@ class TicketCRUD:
return False
ticket.status = status
ticket.updated_at = datetime.now(UTC)
ticket.updated_at = datetime.utcnow()
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.now(UTC)
ticket.updated_at = datetime.utcnow()
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.now(UTC))
return await TicketCRUD.update_ticket_status(db, ticket_id, TicketStatus.CLOSED.value, datetime.utcnow())
@staticmethod
async def close_all_open_tickets(
@@ -280,7 +280,7 @@ class TicketCRUD:
if not ticket_ids:
return []
now = datetime.now(UTC)
now = datetime.utcnow()
await db.execute(
update(Ticket)
.where(Ticket.id.in_(ticket_ids))
@@ -417,7 +417,7 @@ class TicketMessageCRUD:
except Exception:
pass
ticket.updated_at = datetime.now(UTC)
ticket.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(message)
+6 -6
View File
@@ -1,6 +1,6 @@
"""CRUD operations for TicketNotification."""
from datetime import UTC, datetime
from datetime import 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.now(UTC),
created_at=datetime.utcnow(),
)
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.now(UTC))
.values(is_read=True, read_at=datetime.utcnow())
)
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.now(UTC))
.values(is_read=True, read_at=datetime.utcnow())
)
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.now(UTC))
.values(is_read=True, read_at=datetime.utcnow())
)
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.now(UTC))
.values(is_read=True, read_at=datetime.utcnow())
)
if is_admin:
+11 -11
View File
@@ -1,4 +1,4 @@
from datetime import UTC, datetime, timedelta
from datetime import 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.now(UTC) if is_completed else None,
completed_at=datetime.utcnow() if is_completed else None,
**({'created_at': created_at} if created_at else {}),
)
@@ -150,7 +150,7 @@ async def get_user_transactions_count(
async def get_user_total_spent_kopeks(db: AsyncSession, user_id: int) -> int:
result = await db.execute(
select(func.coalesce(func.sum(func.abs(Transaction.amount_kopeks)), 0)).where(
select(func.coalesce(func.sum(Transaction.amount_kopeks), 0)).where(
and_(
Transaction.user_id == user_id,
Transaction.is_completed.is_(True),
@@ -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.now(UTC)
transaction.completed_at = datetime.utcnow()
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.now(UTC).replace(day=1, hour=0, minute=0, second=0, microsecond=0)
start_date = datetime.utcnow().replace(day=1, hour=0, minute=0, second=0, microsecond=0)
if not end_date:
end_date = datetime.now(UTC)
end_date = datetime.utcnow()
# Доход считаем только по реальным платежам (исключаем колесо, промокоды, админские пополнения)
income_result = await db.execute(
@@ -229,7 +229,7 @@ async def get_transactions_statistics(
total_expenses = expenses_result.scalar()
subscription_income_result = await db.execute(
select(func.coalesce(func.sum(func.abs(Transaction.amount_kopeks)), 0)).where(
select(func.coalesce(func.sum(Transaction.amount_kopeks), 0)).where(
and_(
Transaction.type == TransactionType.SUBSCRIPTION_PAYMENT.value,
Transaction.is_completed == True,
@@ -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.now(UTC).date()
today = datetime.utcnow().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.now(UTC) - timedelta(days=days)
start_date = datetime.utcnow() - 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.now(UTC) - timedelta(hours=24)
cutoff_time = datetime.utcnow() - 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.now(UTC).timestamp())
timestamp = int(datetime.utcnow().timestamp())
external_id = f'donation_{payment_id}_{amount_kopeks}_{timestamp}'
logger.info('Создан уникальный external_id для избежания дубликатов', external_id=external_id)
+28 -28
View File
@@ -86,7 +86,7 @@ async def get_user_by_id(db: AsyncSession, user_id: int) -> User | None:
result = await db.execute(
select(User)
.options(
selectinload(User.subscription).selectinload(Subscription.tariff),
selectinload(User.subscription),
selectinload(User.user_promo_groups).selectinload(UserPromoGroup.promo_group),
selectinload(User.referrer),
selectinload(User.promo_group),
@@ -106,7 +106,7 @@ async def get_user_by_telegram_id(db: AsyncSession, telegram_id: int) -> User |
result = await db.execute(
select(User)
.options(
selectinload(User.subscription).selectinload(Subscription.tariff),
selectinload(User.subscription),
selectinload(User.user_promo_groups).selectinload(UserPromoGroup.promo_group),
selectinload(User.referrer),
selectinload(User.promo_group),
@@ -131,7 +131,7 @@ async def get_user_by_username(db: AsyncSession, username: str) -> User | None:
result = await db.execute(
select(User)
.options(
selectinload(User.subscription).selectinload(Subscription.tariff),
selectinload(User.subscription),
selectinload(User.user_promo_groups).selectinload(UserPromoGroup.promo_group),
selectinload(User.referrer),
selectinload(User.promo_group),
@@ -152,7 +152,7 @@ async def get_user_by_referral_code(db: AsyncSession, referral_code: str) -> Use
result = await db.execute(
select(User)
.options(
selectinload(User.subscription).selectinload(Subscription.tariff),
selectinload(User.subscription),
selectinload(User.promo_group),
selectinload(User.referrer),
)
@@ -171,7 +171,7 @@ async def get_user_by_remnawave_uuid(db: AsyncSession, remnawave_uuid: str) -> U
result = await db.execute(
select(User)
.options(
selectinload(User.subscription).selectinload(Subscription.tariff),
selectinload(User.subscription),
selectinload(User.promo_group),
selectinload(User.referrer),
)
@@ -195,7 +195,7 @@ async def create_unique_referral_code(db: AsyncSession) -> str:
if not existing_user:
return code
timestamp = str(int(datetime.now(UTC).timestamp()))[-6:]
timestamp = str(int(datetime.utcnow().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.now(UTC)
user.updated_at = datetime.utcnow()
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.now(UTC)
user.updated_at = datetime.utcnow()
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.now(UTC)
user.updated_at = datetime.utcnow()
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.now(UTC)
now = datetime.utcnow()
result = await db.execute(
select(User).where(
User.promo_offer_discount_percent > 0,
@@ -719,7 +719,7 @@ async def get_users_list(
order_by_purchase_count: bool = False,
) -> list[User]:
query = select(User).options(
selectinload(User.subscription).selectinload(Subscription.tariff),
selectinload(User.subscription),
selectinload(User.promo_group),
selectinload(User.referrer),
)
@@ -879,7 +879,7 @@ async def get_referrals(db: AsyncSession, user_id: int) -> list[User]:
result = await db.execute(
select(User)
.options(
selectinload(User.subscription).selectinload(Subscription.tariff),
selectinload(User.subscription),
selectinload(User.user_promo_groups).selectinload(UserPromoGroup.promo_group),
selectinload(User.referrer),
selectinload(User.promo_group),
@@ -899,12 +899,12 @@ 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.now(UTC)
now = datetime.utcnow()
base_query = (
select(User)
.options(
selectinload(User.subscription).selectinload(Subscription.tariff),
selectinload(User.subscription),
selectinload(User.promo_group),
selectinload(User.referrer),
)
@@ -961,12 +961,12 @@ 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.now(UTC) - timedelta(days=months * 30)
threshold_date = datetime.utcnow() - timedelta(days=months * 30)
result = await db.execute(
select(User)
.options(
selectinload(User.subscription).selectinload(Subscription.tariff),
selectinload(User.subscription),
selectinload(User.user_promo_groups).selectinload(UserPromoGroup.promo_group),
selectinload(User.referrer),
selectinload(User.promo_group),
@@ -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.now(UTC)
user.updated_at = datetime.utcnow()
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.now(UTC).date()
today = datetime.utcnow().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.now(UTC) - timedelta(days=7)
week_ago = datetime.utcnow() - 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.now(UTC) - timedelta(days=30)
month_ago = datetime.utcnow() - 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.now(UTC)
current_time = datetime.utcnow()
result = await db.execute(
select(User)
@@ -1050,7 +1050,7 @@ async def get_users_with_active_subscriptions(db: AsyncSession) -> list[User]:
Subscription.end_date > current_time,
)
)
.options(selectinload(User.subscription).selectinload(Subscription.tariff))
.options(selectinload(User.subscription))
)
return result.scalars().unique().all()
@@ -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.now(UTC)
user.updated_at = datetime.utcnow()
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.now(UTC) > user.email_change_expires:
if user.email_change_expires and datetime.utcnow() > 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.now(UTC)
user.email_verified_at = datetime.utcnow()
user.email_change_new = None
user.email_change_code = None
user.email_change_expires = None
user.updated_at = datetime.now(UTC)
user.updated_at = datetime.utcnow()
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.now(UTC)
user.updated_at = datetime.utcnow()
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)
user.updated_at = datetime.now(UTC).replace(tzinfo=None)
logger.info('Linked (id=) to user', provider=provider, provider_id=provider_id, user_id=user.id)
+3 -3
View File
@@ -1,5 +1,5 @@
import random
from datetime import UTC, datetime
from datetime import 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.now(UTC)
message.updated_at = datetime.utcnow()
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.now(UTC)
message.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(message)
+2 -2
View File
@@ -1,6 +1,6 @@
"""CRUD операции для связи пользователей с промогруппами (Many-to-Many)."""
from datetime import UTC, datetime
from datetime import 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.now(UTC)
user.updated_at = datetime.utcnow()
except Exception as error:
logger.error('Ошибка синхронизации primary промогруппы пользователя', user_id=user_id, error=error)
+3 -3
View File
@@ -3,7 +3,7 @@
from __future__ import annotations
from collections.abc import Iterable
from datetime import UTC, datetime
from datetime import 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.now(UTC)
token.updated_at = datetime.utcnow()
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.now(UTC))
.values(is_active=is_active, updated_at=datetime.utcnow())
)
+4 -4
View File
@@ -1,6 +1,6 @@
from __future__ import annotations
from datetime import UTC, datetime
from datetime import 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.now(UTC)
webhook.updated_at = datetime.utcnow()
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.now(UTC) if status == 'success' else None,
delivered_at=datetime.utcnow() 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.now(UTC)
webhook.last_triggered_at = datetime.utcnow()
await db.commit()
await db.refresh(webhook)
return webhook
+3 -3
View File
@@ -1,4 +1,4 @@
from datetime import UTC, datetime
from datetime import 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.now(UTC)
welcome_text.updated_at = datetime.utcnow()
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.now(UTC)
welcome_text.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(welcome_text)
+6 -6
View File
@@ -2,7 +2,7 @@
CRUD операции для колеса удачи (Fortune Wheel).
"""
from datetime import UTC, datetime
from datetime import 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.now(UTC)
config.updated_at = datetime.utcnow()
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.now(UTC)
prize.updated_at = datetime.utcnow()
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.now(UTC) if is_applied else None,
applied_at=datetime.utcnow() 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.now(UTC)
spin.applied_at = datetime.utcnow()
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.now(UTC).replace(hour=0, minute=0, second=0, microsecond=0)
today_start = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
result = await db.execute(
select(func.count(WheelSpin.id)).where(
+3 -3
View File
@@ -1,4 +1,4 @@
from datetime import UTC, datetime
from datetime import 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.now(UTC)}
update_data = {'status': status, 'is_paid': is_paid, 'is_captured': is_captured, 'updated_at': datetime.utcnow()}
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.now(UTC))
.values(transaction_id=transaction_id, updated_at=datetime.utcnow())
)
await db.commit()
+185 -184
View File
@@ -1,4 +1,4 @@
from datetime import UTC, datetime, time, timedelta
from datetime import 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(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)
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)
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(timezone=True), nullable=True)
paid_at = Column(DateTime, nullable=True)
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, 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(timezone=True), nullable=True)
expires_at = Column(DateTime(timezone=True), nullable=True)
paid_at = Column(DateTime, nullable=True)
expires_at = Column(DateTime, nullable=True)
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, 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(timezone=True), nullable=True)
paid_at = Column(DateTime, 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(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, 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(timezone=True), nullable=True)
paid_at = Column(DateTime, nullable=True)
last_status = Column(String(50), nullable=True)
last_status_checked_at = Column(DateTime(timezone=True), nullable=True)
last_status_checked_at = Column(DateTime, 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(timezone=True), nullable=True)
expires_at = Column(DateTime, nullable=True)
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, 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(timezone=True), nullable=True)
paid_at = Column(DateTime, 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(timezone=True), nullable=True)
expires_at = Column(DateTime, nullable=True)
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, 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(timezone=True), nullable=True)
paid_at = Column(DateTime, 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(timezone=True), nullable=True)
expires_at = Column(DateTime, nullable=True)
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, 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(timezone=True), nullable=True)
paid_at = Column(DateTime, 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(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, 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(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())
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())
# Связь с транзакцией
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(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())
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())
# Связь с транзакцией
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(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, 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(timezone=True), default=func.now())
assigned_at = Column(DateTime, 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(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, 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(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())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
last_activity = Column(DateTime, 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(timezone=True), nullable=True)
email_verified_at = Column(DateTime, nullable=True)
password_hash = Column(String(255), nullable=True)
email_verification_token = Column(String(255), nullable=True)
email_verification_expires = Column(DateTime(timezone=True), nullable=True)
email_verification_expires = Column(DateTime, nullable=True)
password_reset_token = Column(String(255), nullable=True)
password_reset_expires = Column(DateTime(timezone=True), nullable=True)
cabinet_last_login = Column(DateTime(timezone=True), nullable=True)
password_reset_expires = Column(DateTime, nullable=True)
cabinet_last_login = Column(DateTime, 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(timezone=True), nullable=True) # Code expiration
email_change_expires = Column(DateTime, 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(timezone=True), nullable=True)
last_remnawave_sync = Column(DateTime(timezone=True), nullable=True)
promo_offer_discount_expires_at = Column(DateTime, nullable=True)
last_remnawave_sync = Column(DateTime, 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(timezone=True), default=func.now())
end_date = Column(DateTime(timezone=True), nullable=False)
start_date = Column(DateTime, default=func.now())
end_date = Column(DateTime, 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(timezone=True), nullable=True
DateTime, 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(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
last_webhook_update_at = Column(DateTime(timezone=True), nullable=True)
last_webhook_update_at = Column(DateTime, 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(timezone=True), nullable=True) # Время последнего суточного списания
last_daily_charge_at = Column(DateTime, 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.now(UTC)
current_time = datetime.utcnow()
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.now(UTC)
return self.end_date is not None and self.end_date <= datetime.utcnow()
@property
def should_be_expired(self) -> bool:
current_time = datetime.now(UTC)
current_time = datetime.utcnow()
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.now(UTC)
current_time = datetime.utcnow()
if self.status == SubscriptionStatus.EXPIRED.value:
return 'expired'
@@ -1208,6 +1208,7 @@ class Subscription(Base):
@property
def status_display(self) -> str:
actual_status = self.actual_status
datetime.utcnow()
if actual_status == 'expired':
return '🔴 Истекла'
@@ -1243,7 +1244,7 @@ class Subscription(Base):
def days_left(self) -> int:
if self.end_date is None:
return 0
current_time = datetime.now(UTC)
current_time = datetime.utcnow()
if self.end_date <= current_time:
return 0
delta = self.end_date - current_time
@@ -1251,7 +1252,7 @@ class Subscription(Base):
@property
def time_left_display(self) -> str:
current_time = datetime.now(UTC)
current_time = datetime.utcnow()
if self.end_date <= current_time:
return 'истёк'
@@ -1274,10 +1275,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.now(UTC):
if self.end_date > datetime.utcnow():
self.end_date = self.end_date + timedelta(days=days)
else:
self.end_date = datetime.now(UTC) + timedelta(days=days)
self.end_date = datetime.utcnow() + timedelta(days=days)
if self.status == SubscriptionStatus.EXPIRED.value:
self.status = SubscriptionStatus.ACTIVE.value
@@ -1322,16 +1323,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(timezone=True), nullable=False, index=True) # Дата истечения (покупка + 30 дней)
expires_at = Column(DateTime, nullable=False, index=True) # Дата истечения (покупка + 30 дней)
created_at = Column(DateTime(timezone=True), default=func.now())
created_at = Column(DateTime, default=func.now())
subscription = relationship('Subscription', back_populates='traffic_purchases')
@property
def is_expired(self) -> bool:
"""Проверяет, истекла ли докупка."""
return datetime.now(UTC) >= self.expires_at
return datetime.utcnow() >= self.expires_at
class Transaction(Base):
@@ -1351,10 +1352,10 @@ class Transaction(Base):
# NaloGO чек
receipt_uuid = Column(String(255), nullable=True, index=True)
receipt_created_at = Column(DateTime(timezone=True), nullable=True)
receipt_created_at = Column(DateTime, nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
completed_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime, default=func.now())
completed_at = Column(DateTime, nullable=True)
user = relationship('User', back_populates='transactions')
@@ -1369,7 +1370,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(timezone=True), default=func.now())
converted_at = Column(DateTime, default=func.now())
trial_duration_days = Column(Integer, nullable=True)
@@ -1379,7 +1380,7 @@ class SubscriptionConversion(Base):
first_paid_period_days = Column(Integer, nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
created_at = Column(DateTime, default=func.now())
user = relationship('User', backref='subscription_conversions')
@@ -1405,8 +1406,8 @@ class PromoCode(Base):
max_uses = Column(Integer, default=1)
current_uses = Column(Integer, default=0)
valid_from = Column(DateTime(timezone=True), default=func.now())
valid_until = Column(DateTime(timezone=True), nullable=True)
valid_from = Column(DateTime, default=func.now())
valid_until = Column(DateTime, nullable=True)
is_active = Column(Boolean, default=True)
first_purchase_only = Column(Boolean, default=False) # Только для первой покупки
@@ -1414,15 +1415,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(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
uses = relationship('PromoCodeUse', back_populates='promocode')
promo_group = relationship('PromoGroup')
@property
def is_valid(self) -> bool:
now = datetime.now(UTC)
now = datetime.utcnow()
return (
self.is_active
and self.current_uses < self.max_uses
@@ -1442,7 +1443,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(timezone=True), default=func.now())
used_at = Column(DateTime, default=func.now())
promocode = relationship('PromoCode', back_populates='uses')
user = relationship('User')
@@ -1460,7 +1461,7 @@ class ReferralEarning(Base):
referral_transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
created_at = Column(DateTime, default=func.now())
user = relationship('User', foreign_keys=[user_id], back_populates='referral_earnings')
referral = relationship('User', foreign_keys=[referral_id])
@@ -1501,11 +1502,11 @@ class WithdrawalRequest(Base):
# Обработка админом
processed_by = Column(Integer, ForeignKey('users.id'), nullable=True)
processed_at = Column(DateTime(timezone=True), nullable=True)
processed_at = Column(DateTime, nullable=True)
admin_comment = Column(Text, nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
user = relationship('User', foreign_keys=[user_id], backref='withdrawal_requests')
admin = relationship('User', foreign_keys=[processed_by])
@@ -1523,18 +1524,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(timezone=True), nullable=False)
end_at = Column(DateTime(timezone=True), nullable=False)
start_at = Column(DateTime, nullable=False)
end_at = Column(DateTime, 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(timezone=True), nullable=True)
last_daily_summary_at = Column(DateTime, 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(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
creator = relationship('User', backref='created_referral_contests')
events = relationship(
@@ -1564,7 +1565,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(timezone=True), nullable=False, default=func.now())
occurred_at = Column(DateTime, nullable=False, default=func.now())
contest = relationship('ReferralContest', back_populates='events')
referrer = relationship('User', foreign_keys=[referrer_id])
@@ -1584,7 +1585,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(timezone=True), default=func.now())
created_at = Column(DateTime, default=func.now())
contest = relationship('ReferralContest')
@@ -1610,8 +1611,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(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
rounds = relationship('ContestRound', back_populates='template')
@@ -1625,8 +1626,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(timezone=True), nullable=False)
ends_at = Column(DateTime(timezone=True), nullable=False)
starts_at = Column(DateTime, nullable=False)
ends_at = Column(DateTime, 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)
@@ -1634,8 +1635,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(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
template = relationship('ContestTemplate', back_populates='rounds')
attempts = relationship('ContestAttempt', back_populates='round', cascade='all, delete-orphan')
@@ -1653,7 +1654,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(timezone=True), default=func.now())
created_at = Column(DateTime, default=func.now())
round = relationship('ContestRound', back_populates='attempts')
user = relationship('User')
@@ -1673,8 +1674,8 @@ class Squad(Base):
description = Column(Text, nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
@property
def price_rubles(self) -> float:
@@ -1695,8 +1696,8 @@ class ServiceRule(Base):
language = Column(String(5), default='ru')
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
class PrivacyPolicy(Base):
@@ -1706,8 +1707,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(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
class PublicOffer(Base):
@@ -1717,8 +1718,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(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
class FaqSetting(Base):
@@ -1727,8 +1728,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(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
class FaqPage(Base):
@@ -1740,8 +1741,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(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
class SystemSetting(Base):
@@ -1752,8 +1753,8 @@ class SystemSetting(Base):
value = Column(Text, nullable=True)
description = Column(Text, nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
class MonitoringLog(Base):
@@ -1768,7 +1769,7 @@ class MonitoringLog(Base):
is_success = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), default=func.now())
created_at = Column(DateTime, default=func.now())
class SentNotification(Base):
@@ -1779,7 +1780,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(timezone=True), default=func.now())
created_at = Column(DateTime, default=func.now())
user = relationship('User', backref='sent_notifications')
subscription = relationship('Subscription', backref=backref('sent_notifications', passive_deletes=True))
@@ -1796,9 +1797,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(timezone=True), nullable=False, default=func.now())
occurred_at = Column(DateTime, nullable=False, default=func.now())
extra = Column(JSON, nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
created_at = Column(DateTime, default=func.now())
user = relationship('User', backref='subscription_events')
subscription = relationship('Subscription', backref='subscription_events')
@@ -1815,13 +1816,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(timezone=True), nullable=False)
claimed_at = Column(DateTime(timezone=True), nullable=True)
expires_at = Column(DateTime, nullable=False)
claimed_at = Column(DateTime, 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(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
user = relationship('User', back_populates='discount_offers')
subscription = relationship('Subscription', back_populates='discount_offers')
@@ -1845,8 +1846,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(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
creator = relationship('User')
@@ -1858,9 +1859,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(timezone=True), nullable=False)
created_at = Column(DateTime(timezone=True), default=func.now())
deactivated_at = Column(DateTime(timezone=True), nullable=True)
expires_at = Column(DateTime, nullable=False)
created_at = Column(DateTime, default=func.now())
deactivated_at = Column(DateTime, nullable=True)
is_active = Column(Boolean, default=True, nullable=False)
was_already_connected = Column(Boolean, default=False, nullable=False)
@@ -1879,7 +1880,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(timezone=True), default=func.now())
created_at = Column(DateTime, default=func.now())
user = relationship('User', back_populates='promo_offer_logs')
offer = relationship('DiscountOffer', back_populates='logs')
@@ -1921,8 +1922,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(timezone=True), default=func.now(), nullable=False)
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now(), nullable=False)
created_at = Column(DateTime, default=func.now(), nullable=False)
updated_at = Column(DateTime, default=func.now(), onupdate=func.now(), nullable=False)
creator = relationship('User', backref='created_polls', foreign_keys=[created_by])
questions = relationship(
@@ -1974,9 +1975,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(timezone=True), default=func.now(), nullable=False)
started_at = Column(DateTime(timezone=True), nullable=True)
completed_at = Column(DateTime(timezone=True), nullable=True)
sent_at = Column(DateTime, default=func.now(), nullable=False)
started_at = Column(DateTime, nullable=True)
completed_at = Column(DateTime, nullable=True)
reward_given = Column(Boolean, nullable=False, default=False)
reward_amount_kopeks = Column(Integer, nullable=False, default=0)
@@ -1998,7 +1999,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(timezone=True), default=func.now(), nullable=False)
created_at = Column(DateTime, default=func.now(), nullable=False)
response = relationship('PollResponse', back_populates='answers')
question = relationship('PollQuestion', back_populates='answers')
@@ -2032,8 +2033,8 @@ class ServerSquad(Base):
max_users = Column(Integer, nullable=True)
current_users = Column(Integer, default=0)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
allowed_promo_groups = relationship(
'PromoGroup',
@@ -2068,7 +2069,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(timezone=True), default=func.now())
connected_at = Column(DateTime, default=func.now())
paid_price_kopeks = Column(Integer, default=0)
@@ -2087,7 +2088,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(timezone=True), default=func.now())
created_at = Column(DateTime, default=func.now())
actor = relationship('User', foreign_keys=[actor_user_id])
ticket = relationship('Ticket', foreign_keys=[ticket_id])
@@ -2100,8 +2101,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(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
creator = relationship('User', backref='created_messages')
def __repr__(self):
@@ -2116,8 +2117,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(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
creator = relationship('User', backref='created_welcome_texts')
@@ -2133,8 +2134,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(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
creator = relationship('User', backref='pinned_messages')
@@ -2161,8 +2162,8 @@ class AdvertisingCampaign(Base):
is_active = Column(Boolean, default=True)
created_by = Column(Integer, ForeignKey('users.id'), nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
registrations = relationship('AdvertisingCampaignRegistration', back_populates='campaign')
tariff = relationship('Tariff', foreign_keys=[tariff_id])
@@ -2202,7 +2203,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(timezone=True), default=func.now())
created_at = Column(DateTime, default=func.now())
campaign = relationship('AdvertisingCampaign', back_populates='registrations')
user = relationship('User')
@@ -2231,13 +2232,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(timezone=True), nullable=True)
user_reply_block_until = 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)
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
closed_at = Column(DateTime, nullable=True)
# SLA reminders
last_sla_reminder_at = Column(DateTime(timezone=True), nullable=True)
last_sla_reminder_at = Column(DateTime, nullable=True)
# Связи
user = relationship('User', backref='tickets')
@@ -2265,9 +2266,9 @@ class Ticket(Base):
return True
if self.user_reply_block_until:
try:
from datetime import UTC, datetime
from datetime import datetime
return self.user_reply_block_until > datetime.now(UTC)
return self.user_reply_block_until > datetime.utcnow()
except Exception:
return True
return False
@@ -2307,7 +2308,7 @@ class TicketMessage(Base):
media_file_id = Column(String(255), nullable=True)
media_caption = Column(Text, nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
created_at = Column(DateTime, default=func.now())
# Связи
ticket = relationship('Ticket', back_populates='messages')
@@ -2333,10 +2334,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(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)
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)
last_used_ip = Column(String(64), nullable=True)
is_active = Column(Boolean, default=True, nullable=False)
created_by = Column(String(255), nullable=True)
@@ -2356,8 +2357,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(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
__table_args__ = (Index('ix_main_menu_buttons_order', 'display_order', 'id'),)
@@ -2392,7 +2393,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(timezone=True), default=func.now(), index=True)
created_at = Column(DateTime, default=func.now(), index=True)
__table_args__ = (Index('ix_menu_layout_history_created', 'created_at'),)
@@ -2409,7 +2410,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(timezone=True), default=func.now(), index=True)
clicked_at = Column(DateTime, default=func.now(), index=True)
# Дополнительная информация
button_type = Column(String(20), nullable=True, index=True) # builtin, callback, url, mini_app
@@ -2443,9 +2444,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(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)
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
last_triggered_at = Column(DateTime, nullable=True)
failure_count = Column(Integer, default=0, nullable=False)
success_count = Column(Integer, default=0, nullable=False)
@@ -2474,9 +2475,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(timezone=True), default=func.now())
delivered_at = Column(DateTime(timezone=True), nullable=True)
next_retry_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime, default=func.now())
delivered_at = Column(DateTime, nullable=True)
next_retry_at = Column(DateTime, nullable=True)
webhook = relationship('Webhook', back_populates='deliveries')
@@ -2494,15 +2495,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(timezone=True), nullable=False)
created_at = Column(DateTime(timezone=True), default=func.now())
revoked_at = Column(DateTime(timezone=True), nullable=True)
expires_at = Column(DateTime, nullable=False)
created_at = Column(DateTime, default=func.now())
revoked_at = Column(DateTime, nullable=True)
user = relationship('User', backref='cabinet_tokens')
@property
def is_expired(self) -> bool:
return datetime.now(UTC) > self.expires_at
return datetime.utcnow() > self.expires_at
@property
def is_revoked(self) -> bool:
@@ -2548,8 +2549,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(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
prizes = relationship('WheelPrize', back_populates='config', cascade='all, delete-orphan')
@@ -2587,8 +2588,8 @@ class WheelPrize(Base):
promo_subscription_days = Column(Integer, default=0)
promo_traffic_gb = Column(Integer, default=0)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
config = relationship('WheelConfig', back_populates='prizes')
spins = relationship('WheelSpin', back_populates='prize')
@@ -2623,9 +2624,9 @@ class WheelSpin(Base):
# Флаг успешного начисления
is_applied = Column(Boolean, default=False, nullable=False)
applied_at = Column(DateTime(timezone=True), nullable=True)
applied_at = Column(DateTime, nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
created_at = Column(DateTime, default=func.now())
user = relationship('User', backref='wheel_spins')
prize = relationship('WheelPrize', back_populates='spins')
@@ -2670,8 +2671,8 @@ class TicketNotification(Base):
# Прочитано ли уведомление
is_read = Column(Boolean, default=False, nullable=False)
created_at = Column(DateTime(timezone=True), default=func.now())
read_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime, default=func.now())
read_at = Column(DateTime, nullable=True)
ticket = relationship('Ticket', backref='notifications')
user = relationship('User', backref='ticket_notifications')
@@ -2728,8 +2729,8 @@ class PaymentMethodConfig(Base):
lazy='selectin',
)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
def __repr__(self) -> str:
return f"<PaymentMethodConfig method_id='{self.method_id}' order={self.sort_order} enabled={self.is_enabled}>"
+2 -66
View File
@@ -1,4 +1,4 @@
from datetime import UTC, datetime
from datetime import 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.now(UTC)
existing.updated_at = datetime.utcnow()
await session.commit()
return True
@@ -6454,63 +6454,6 @@ 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('=== НАЧАЛО УНИВЕРСАЛЬНОЙ МИГРАЦИИ ===')
@@ -7187,13 +7130,6 @@ 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'))
+4 -4
View File
@@ -3,7 +3,7 @@ from __future__ import annotations
import asyncio
import base64
import json
from datetime import UTC, datetime, timedelta
from datetime import 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.now(UTC)
now = datetime.utcnow()
if self._cached_key and self._expires_at and now < self._expires_at:
return self._cached_key
async with self._lock:
now = datetime.now(UTC)
now = datetime.utcnow()
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.now(UTC) + timedelta(seconds=self._cache_seconds)
self._expires_at = datetime.utcnow() + timedelta(seconds=self._cache_seconds)
else:
self._expires_at = None
logger.debug('Получен и закеширован публичный ключ WATA')
+3 -3
View File
@@ -3,7 +3,7 @@ import io
import math
import time
from collections.abc import Iterable
from datetime import UTC, datetime
from datetime import 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.now(UTC).strftime("%Y-%m-%d %H:%M:%S UTC")}',
f'# Generated at {datetime.utcnow().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.now(UTC).strftime("%Y%m%d-%H%M%S")}.env'
filename = f'bot-settings-{datetime.utcnow().strftime("%Y%m%d-%H%M%S")}.env'
file = types.BufferedInputFile(content.encode('utf-8'), filename=filename)
await callback.message.answer_document(
+3 -3
View File
@@ -399,7 +399,7 @@ async def delete_contest(
await callback.answer(texts.t('ADMIN_CONTEST_NOT_FOUND', 'Конкурс не найден.'), show_alert=True)
return
now_utc = datetime.now(UTC)
now_utc = datetime.utcnow()
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)
end_at = datetime.fromisoformat(end_at_raw).astimezone(UTC)
start_at = datetime.fromisoformat(start_at_raw).astimezone(UTC).replace(tzinfo=None)
end_at = datetime.fromisoformat(end_at_raw).astimezone(UTC).replace(tzinfo=None)
contest_type = data.get('contest_type') or 'referral_paid'
+10 -10
View File
@@ -1,5 +1,5 @@
import json
from datetime import UTC, datetime, timedelta
from datetime import 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.now(UTC)
now = datetime.utcnow()
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,
ends,
now.replace(tzinfo=None),
ends.replace(tzinfo=None),
)
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.now(UTC)
now = datetime.utcnow()
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,
ends,
now.replace(tzinfo=None),
ends.replace(tzinfo=None),
)
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.now(UTC)
now = datetime.utcnow()
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,
ends,
now.replace(tzinfo=None),
ends.replace(tzinfo=None),
)
started_count += 1
+20 -20
View File
@@ -1,6 +1,6 @@
import asyncio
import html
from datetime import UTC, datetime, timedelta
from datetime import 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.now(UTC)
completed_at = datetime.utcnow()
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.now(UTC)
pinned_message.updated_at = datetime.utcnow()
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.now(UTC)
pinned_message.updated_at = datetime.utcnow()
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 UTC, datetime, timedelta
from datetime import 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.now(UTC)
now = datetime.utcnow()
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.now(UTC)
now = datetime.utcnow()
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.now(UTC)
now = datetime.utcnow()
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.now(UTC)
now = datetime.utcnow()
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.now(UTC)
now = datetime.utcnow()
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.now(UTC)
now = datetime.utcnow()
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.now(UTC)
now = datetime.utcnow()
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.now(UTC)
now = datetime.utcnow()
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.now(UTC)
now = datetime.utcnow()
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.now(UTC) - timedelta(days=7)
week_ago = datetime.utcnow() - 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.now(UTC) - timedelta(days=30)
threshold = datetime.utcnow() - timedelta(days=30)
return [user for user in users if user.last_activity and user.last_activity < threshold]
if target == 'inactive_60d':
threshold = datetime.now(UTC) - timedelta(days=60)
threshold = datetime.utcnow() - timedelta(days=60)
return [user for user in users if user.last_activity and user.last_activity < threshold]
if target == 'inactive_90d':
threshold = datetime.now(UTC) - timedelta(days=90)
threshold = datetime.utcnow() - 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.now(UTC)
now = datetime.utcnow()
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.now(UTC)
now = datetime.utcnow()
today = now.replace(hour=0, minute=0, second=0, microsecond=0)
week_ago = now - timedelta(days=7)
month_ago = now - timedelta(days=30)
+10 -13
View File
@@ -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(UTC)
now = datetime.now()
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']}
🕐 <b>Время проверки:</b> {datetime.now(UTC).strftime('%H:%M:%S')}
🕐 <b>Время проверки:</b> {datetime.now().strftime('%H:%M:%S')}
Нажмите "Назад" для возврата в меню мониторинга.
"""
@@ -748,7 +748,7 @@ async def traffic_check_callback(callback: CallbackQuery):
Порог дельты: {threshold_gb} ГБ
Возраст snapshot: {snapshot_age:.1f} мин
🕐 <b>Время проверки:</b> {datetime.now(UTC).strftime('%H:%M:%S')}
🕐 <b>Время проверки:</b> {datetime.now().strftime('%H:%M:%S')}
"""
if violations:
@@ -862,7 +862,7 @@ async def test_notifications_callback(callback: CallbackQuery):
📊 <b>Статус системы:</b>
Мониторинг: {'🟢 Работает' if monitoring_service.is_running else '🔴 Остановлен'}
Уведомления: {'🟢 Включены' if settings.ENABLE_NOTIFICATIONS else '🔴 Отключены'}
Время теста: {datetime.now(UTC).strftime('%H:%M:%S %d.%m.%Y')}
Время теста: {datetime.now().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(UTC) - timedelta(days=7)
week_ago = datetime.now() - 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(UTC) - timedelta(days=7)
week_ago = datetime.now() - 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 UTC, date, timedelta
from datetime import 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, tzinfo=UTC)
TRACKING_START_DATE = datetime(2024, 12, 29, 0, 0, 0)
async with AsyncSessionLocal() as db:
# Получаем старые транзакции без чеков
@@ -1311,12 +1311,9 @@ async def receipts_link_old_callback(callback: CallbackQuery):
try:
from dateutil.parser import isoparse
parsed_time = isoparse(operation_time)
t.receipt_created_at = (
parsed_time if parsed_time.tzinfo else parsed_time.replace(tzinfo=UTC)
)
t.receipt_created_at = isoparse(operation_time)
except Exception:
t.receipt_created_at = datetime.now(UTC)
t.receipt_created_at = datetime.utcnow()
linked += 1
if linked > 0:
+2 -2
View File
@@ -822,9 +822,9 @@ async def export_payments(
file_bytes = json_content.encode('utf-8')
# Отправляем файл
from datetime import UTC, datetime
from datetime import datetime
filename = f'payments_export_{datetime.now(UTC).strftime("%Y%m%d_%H%M%S")}.json'
filename = f'payments_export_{datetime.now().strftime("%Y%m%d_%H%M%S")}.json'
await callback.message.answer_document(
document=BufferedInputFile(file_bytes, filename=filename),
+4 -4
View File
@@ -4,7 +4,7 @@ import asyncio
import html
import re
from collections.abc import Sequence
from datetime import UTC, datetime
from datetime import 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.now(UTC)).total_seconds())
remaining_seconds = int((offer.expires_at - datetime.utcnow()).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.now(UTC)
now = datetime.utcnow()
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.now(UTC)
now = datetime.utcnow()
result = await db.execute(
select(SubscriptionTemporaryAccess)
.options(selectinload(SubscriptionTemporaryAccess.offer))
+4 -4
View File
@@ -1,4 +1,4 @@
from datetime import UTC, datetime, timedelta
from datetime import 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.now(UTC) + timedelta(days=expiry_days)
valid_until = datetime.utcnow() + 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.now(UTC) + timedelta(days=expiry_days)
valid_until = datetime.utcnow() + 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.now(UTC) + timedelta(days=expiry_days)
valid_until = datetime.utcnow() + timedelta(days=expiry_days)
await update_promocode(db, promo, valid_until=valid_until)
+5 -6
View File
@@ -1,6 +1,5 @@
import datetime
import json
from datetime import UTC
import structlog
from aiogram import Dispatcher, F, types
@@ -34,7 +33,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(UTC).strftime('%H:%M:%S')
current_time = datetime.datetime.now().strftime('%H:%M:%S')
text = f"""
🤝 <b>Реферальная статистика</b>
@@ -115,7 +114,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(UTC).strftime('%H:%M:%S')
current_time = datetime.datetime.now().strftime('%H:%M:%S')
text = f"""
🤝 <b>Реферальная статистика</b>
@@ -657,7 +656,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(UTC)
now = datetime.datetime.now()
today = now.replace(hour=0, minute=0, second=0, microsecond=0)
if period == 'today':
@@ -1154,9 +1153,9 @@ async def sync_referrals_with_contest(
await callback.answer('🏆 Синхронизирую с конкурсами...')
try:
from datetime import UTC, datetime
from datetime import datetime
now_utc = datetime.now(UTC)
now_utc = datetime.utcnow()
# Получаем активные конкурсы
paid_contests = await get_contests_for_events(db, now_utc, contest_types=['referral_paid'])
+4 -4
View File
@@ -1,5 +1,5 @@
import math
from datetime import UTC, datetime
from datetime import 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"""
🕒 <b>Обновлено:</b> {format_datetime(stats.get('last_updated', datetime.now(UTC)))}
🕒 <b>Обновлено:</b> {format_datetime(stats.get('last_updated', datetime.now()))}
"""
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🕒 <b>Обновлено:</b> {format_datetime(datetime.now(UTC))}'
text += f'\n🕒 <b>Обновлено:</b> {format_datetime(datetime.now())}'
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(UTC)
end_date = datetime.now()
start_date = end_date - timedelta(days=7)
node_usage = await remnawave_service.get_node_user_usage_by_range(node_uuid, start_date, end_date)
+10 -10
View File
@@ -1,4 +1,4 @@
from datetime import UTC, datetime, timedelta
from datetime import 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.now(UTC))
current_time = format_datetime(datetime.utcnow())
text = f"""
👥 <b>Статистика пользователей</b>
@@ -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.now(UTC))
current_time = format_datetime(datetime.utcnow())
text = f"""
📱 <b>Статистика подписок</b>
@@ -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.now(UTC)
now = datetime.utcnow()
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.now(UTC))
current_time = format_datetime(datetime.utcnow())
text = f"""
💰 <b>Статистика доходов</b>
@@ -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.now(UTC))
current_time = format_datetime(datetime.utcnow())
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.now(UTC)
now = datetime.utcnow()
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.now(UTC))
current_time = format_datetime(datetime.utcnow())
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.now(UTC).date() - timedelta(days=1)
yesterday = datetime.utcnow().date() - timedelta(days=1)
revenue_data = [r for r in revenue_data if r['date'] == yesterday]
elif period == 'today':
today = datetime.now(UTC).date()
today = datetime.utcnow().date()
revenue_data = [r for r in revenue_data if r['date'] == today]
total_revenue = sum(r['amount_kopeks'] for r in revenue_data)
+3 -3
View File
@@ -1,4 +1,4 @@
from datetime import UTC, datetime
from datetime import 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, tz=UTC)
updated_at = datetime.fromtimestamp(stats.st_mtime)
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, tz=UTC).strftime('%d.%m.%Y %H:%M:%S')
updated_at = datetime.fromtimestamp(stats.st_mtime).strftime('%d.%m.%Y %H:%M:%S')
caption = (
f'🧾 Лог-файл <code>{log_path.name}</code>\n📁 Путь: <code>{log_path}</code>\n🕒 Обновлен: {updated_at}'
)
+3 -3
View File
@@ -1,6 +1,6 @@
import html
import time
from datetime import UTC, datetime, timedelta
from datetime import 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.now(UTC) + timedelta(minutes=minutes)
until = datetime.utcnow() + 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.now(UTC) + timedelta(minutes=minutes)
until = datetime.utcnow() + timedelta(minutes=minutes)
ok = await TicketCRUD.set_user_reply_block(db, ticket_id, permanent=False, until=until)
if not ok:
await message.answer('❌ Ошибка блокировки')
+18 -17
View File
@@ -1,6 +1,6 @@
import re
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from datetime import 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.now(UTC)).days
days_left = (user.subscription.end_date - datetime.utcnow()).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.now(UTC)
current_time = datetime.utcnow()
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.now(UTC)
current_time = datetime.utcnow()
active_subscription_query = (
select(func.count(Subscription.id))
@@ -859,7 +859,7 @@ async def _render_user_subscription_overview(callback: types.CallbackQuery, db:
text += f'<b>Устройства:</b> {subscription.device_limit}\n'
if subscription.is_active:
days_left = (subscription.end_date - datetime.now(UTC)).days
days_left = (subscription.end_date - datetime.utcnow()).days
text += f'<b>Осталось дней:</b> {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.now(UTC)
user.updated_at = datetime.utcnow()
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.now(UTC)
current_time = datetime.utcnow()
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.now(UTC)
subscription.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(subscription)
@@ -3628,8 +3628,9 @@ async def set_user_devices_button(callback: types.CallbackQuery, db_user: User,
await callback.answer()
logger.info(
'Админ изменил устройства для пользователя', telegram_id=db_user.telegram_id, devices=devices, user_id=user_id
'Админ модем для пользователя', telegram_id=db_user.telegram_id, action_text=action_text, user_id=user_id
)
await callback.answer()
@admin_required
@@ -3872,7 +3873,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.now(UTC)
subscription.updated_at = datetime.utcnow()
await db.commit()
@@ -3916,7 +3917,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.now(UTC)
subscription.updated_at = datetime.utcnow()
await db.commit()
@@ -4038,7 +4039,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 UTC, datetime
from datetime import datetime
from app.database.crud.subscription import get_subscription_by_user_id
from app.database.models import SubscriptionStatus
@@ -4050,8 +4051,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.now(UTC):
subscription.end_date = datetime.now(UTC) + timedelta(days=1)
if subscription.end_date <= datetime.utcnow():
subscription.end_date = datetime.utcnow() + timedelta(days=1)
await db.commit()
await db.refresh(subscription)
@@ -4506,7 +4507,7 @@ async def admin_buy_subscription_execute(callback: types.CallbackQuery, db_user:
return
if subscription:
current_time = datetime.now(UTC)
current_time = datetime.utcnow()
bonus_period = timedelta()
if subscription.is_trial and settings.TRIAL_ADD_REMAINING_DAYS_TO_PAID and subscription.end_date:
@@ -5102,7 +5103,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.now(UTC)
subscription.updated_at = datetime.utcnow()
if not new_is_trial and subscription.is_trial:
user = await get_user_by_id(db, user_id)
@@ -5306,7 +5307,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.now(UTC)
subscription.updated_at = datetime.utcnow()
# Сбрасываем докупленный трафик при смене тарифа
from sqlalchemy import delete as sql_delete
+2 -2
View File
@@ -1,4 +1,4 @@
from datetime import UTC, datetime
from datetime import 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.now(UTC))
.values(metadata_json=metadata, updated_at=datetime.utcnow())
)
await db.commit()
except Exception as error: # pragma: no cover - diagnostics
+1 -1
View File
@@ -279,7 +279,7 @@ async def show_balance_history(callback: types.CallbackQuery, db_user: User, db:
amount_text = (
f'+{texts.format_price(transaction.amount_kopeks)}'
if transaction.type == TransactionType.DEPOSIT.value
else f'-{texts.format_price(abs(transaction.amount_kopeks))}'
else f'-{texts.format_price(transaction.amount_kopeks)}'
)
text += f'{emoji} {amount_text}\n'
+2 -2
View File
@@ -1,5 +1,5 @@
import html
from datetime import UTC, datetime
from datetime import 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.now(UTC))
.values(metadata_json=metadata, updated_at=datetime.utcnow())
)
await db.commit()
except Exception as error: # pragma: no cover - diagnostics
+2 -2
View File
@@ -1,4 +1,4 @@
from datetime import UTC, datetime
from datetime import 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.now(UTC))
.values(metadata_json=metadata, updated_at=datetime.utcnow())
)
await db.commit()
except Exception as error: # pragma: no cover - diagnostics
+3 -3
View File
@@ -1,4 +1,4 @@
from datetime import UTC, datetime
from datetime import 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.now(UTC))
.values(metadata_json=metadata, updated_at=datetime.utcnow())
)
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.now(UTC))
.values(metadata_json=metadata, updated_at=datetime.utcnow())
)
await db.commit()
except Exception as error: # pragma: no cover - диагностический лог
+2 -2
View File
@@ -1,6 +1,6 @@
"""Contest handlers for daily games."""
from datetime import UTC, datetime
from datetime import 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.now(UTC).timestamp()
now = datetime.utcnow().timestamp()
if key not in _rate_limits:
_rate_limits[key] = []
+4 -4
View File
@@ -1,5 +1,5 @@
import html
from datetime import UTC, datetime
from datetime import 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.now(UTC)
db_user.last_activity = datetime.utcnow()
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.now(UTC)
current_time = datetime.utcnow()
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.now(UTC):
if subscription and subscription.status == 'ACTIVE' and subscription.end_date > datetime.utcnow():
await callback.answer(
texts.t('SUBSCRIPTION_ALREADY_ACTIVE', '✅ Подписка уже активна!'),
show_alert=True,
+3 -3
View File
@@ -1,5 +1,5 @@
import asyncio
from datetime import UTC, datetime
from datetime import 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.now(UTC)
response.started_at = datetime.utcnow()
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.now(UTC)
response.completed_at = datetime.utcnow()
await db.commit()
reward_amount = await reward_user_for_poll(db, response)
+1 -1
View File
@@ -793,7 +793,7 @@ async def confirm_withdrawal_request(callback: types.CallbackQuery, db_user: Use
try:
notification_service = AdminNotificationService(callback.bot)
await notification_service.send_admin_notification(admin_text, reply_markup=admin_keyboard)
await notification_service.send_to_admins(admin_text, keyboard=admin_keyboard)
except Exception as e:
logger.error('Ошибка отправки уведомления админам о заявке на вывод', error=e)
+2 -2
View File
@@ -1,4 +1,4 @@
from datetime import UTC, datetime
from datetime import 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(UTC).strftime('%H:%M:%S')
updated_at = datetime.now().strftime('%H:%M:%S')
lines.extend(
[
+2 -2
View File
@@ -1,7 +1,7 @@
"""Обработчики для простой покупки подписки."""
import html
from datetime import UTC, datetime
from datetime import 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.now(UTC)).days)
days_left = max(0, (current_subscription.end_date - datetime.utcnow()).days)
except Exception:
days_left = 0
key = (
+20 -20
View File
@@ -1,4 +1,4 @@
from datetime import UTC, datetime
from datetime import 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.now(UTC)
user.last_activity = datetime.utcnow()
if profile_updated:
user.updated_at = datetime.now(UTC)
user.updated_at = datetime.utcnow()
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.now(UTC)
user.last_activity = datetime.now(UTC)
user.updated_at = datetime.utcnow()
user.last_activity = datetime.utcnow()
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 UTC, datetime
from datetime import datetime
existing_user.updated_at = datetime.now(UTC)
existing_user.last_activity = datetime.now(UTC)
existing_user.updated_at = datetime.utcnow()
existing_user.last_activity = datetime.utcnow()
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 UTC, datetime
from datetime import datetime
existing_user.updated_at = datetime.now(UTC)
existing_user.last_activity = datetime.now(UTC)
existing_user.updated_at = datetime.utcnow()
existing_user.last_activity = datetime.utcnow()
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 UTC, datetime
from datetime import datetime
existing_user.updated_at = datetime.now(UTC)
existing_user.last_activity = datetime.now(UTC)
existing_user.updated_at = datetime.utcnow()
existing_user.last_activity = datetime.utcnow()
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 UTC, datetime
from datetime import datetime
existing_user.updated_at = datetime.now(UTC)
existing_user.last_activity = datetime.now(UTC)
existing_user.updated_at = datetime.utcnow()
existing_user.last_activity = datetime.utcnow()
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 UTC, datetime
from datetime import 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.now(UTC)
current_time = datetime.utcnow()
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.now(UTC)
subscription.updated_at = datetime.utcnow()
await db.commit()
await db.refresh(subscription)
logger.info(
+3 -3
View File
@@ -1,4 +1,4 @@
from datetime import UTC, datetime
from datetime import 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.now(UTC)
subscription.updated_at = datetime.utcnow()
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.now(UTC)
subscription.updated_at = datetime.utcnow()
await db.commit()
subscription_service = SubscriptionService()
+4 -4
View File
@@ -1,4 +1,4 @@
from datetime import UTC, datetime
from datetime import 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.now(UTC)
now = datetime.utcnow()
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.now(UTC)
subscription.updated_at = datetime.utcnow()
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.now(UTC)
now = datetime.utcnow()
days_left = max(1, (subscription.end_date - now).days)
period_hint_days = days_left
+2 -2
View File
@@ -446,13 +446,13 @@ async def get_subscription_info_text(subscription, texts, db_user, db: AsyncSess
# Отображаем докупленный трафик
if (subscription.traffic_limit_gb or 0) > 0: # Только для лимитированных тарифов
from datetime import UTC, datetime
from datetime import datetime
from sqlalchemy import select as sql_select
from app.database.models import TrafficPurchase
now = datetime.now(UTC)
now = datetime.utcnow()
purchases_query = (
sql_select(TrafficPurchase)
.where(TrafficPurchase.subscription_id == subscription.id)
+2 -2
View File
@@ -1,4 +1,4 @@
from datetime import UTC, datetime, timedelta
from datetime import datetime, timedelta
from typing import Any
from aiogram import types
@@ -126,7 +126,7 @@ async def claim_discount_offer(
)
return
now = datetime.now(UTC)
now = datetime.utcnow()
if offer.claimed_at is not None:
await callback.answer(
texts.get('DISCOUNT_CLAIM_ALREADY', 'ℹ️ Скидка уже была активирована'),

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