fix: complete datetime.utcnow() → datetime.now(UTC) migration
- Migrate 660+ datetime.utcnow() across 153 files to datetime.now(UTC) - Migrate 30+ datetime.now() without UTC to datetime.now(UTC) - Convert all 170 DateTime columns to DateTime(timezone=True) - Add migrate_datetime_to_timestamptz() in universal_migration with SET LOCAL timezone='UTC' safety - Remove 70+ .replace(tzinfo=None) workarounds - Fix utcfromtimestamp → fromtimestamp(..., tz=UTC) - Fix fromtimestamp() without tz= (system_logs, backup_service, referral_diagnostics) - Fix fromisoformat/isoparse to ensure aware output (platega, yookassa, wata, miniapp, nalogo) - Fix strptime() to add .replace(tzinfo=UTC) (backup_service, referral_diagnostics) - Fix datetime.combine() to include tzinfo=UTC (remnawave_sync, traffic_monitoring) - Fix datetime.max/datetime.min sentinels with .replace(tzinfo=UTC) - Rename panel_datetime_to_naive_utc → panel_datetime_to_utc - Remove DTZ003 from ruff ignore list
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
"""Email verification token generation and validation."""
|
"""Email verification token generation and validation."""
|
||||||
|
|
||||||
import secrets
|
import secrets
|
||||||
from datetime import datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ def get_email_change_expires_at() -> datetime:
|
|||||||
Datetime when the email change code expires
|
Datetime when the email change code expires
|
||||||
"""
|
"""
|
||||||
minutes = settings.get_cabinet_email_change_code_expire_minutes()
|
minutes = settings.get_cabinet_email_change_code_expire_minutes()
|
||||||
return datetime.utcnow() + timedelta(minutes=minutes)
|
return datetime.now(UTC) + timedelta(minutes=minutes)
|
||||||
|
|
||||||
|
|
||||||
def generate_verification_token() -> str:
|
def generate_verification_token() -> str:
|
||||||
@@ -55,7 +55,7 @@ def get_verification_expires_at() -> datetime:
|
|||||||
Datetime when the verification token expires
|
Datetime when the verification token expires
|
||||||
"""
|
"""
|
||||||
hours = settings.get_cabinet_email_verification_expire_hours()
|
hours = settings.get_cabinet_email_verification_expire_hours()
|
||||||
return datetime.utcnow() + timedelta(hours=hours)
|
return datetime.now(UTC) + timedelta(hours=hours)
|
||||||
|
|
||||||
|
|
||||||
def get_password_reset_expires_at() -> datetime:
|
def get_password_reset_expires_at() -> datetime:
|
||||||
@@ -66,7 +66,7 @@ def get_password_reset_expires_at() -> datetime:
|
|||||||
Datetime when the password reset token expires
|
Datetime when the password reset token expires
|
||||||
"""
|
"""
|
||||||
hours = settings.get_cabinet_password_reset_expire_hours()
|
hours = settings.get_cabinet_password_reset_expire_hours()
|
||||||
return datetime.utcnow() + timedelta(hours=hours)
|
return datetime.now(UTC) + timedelta(hours=hours)
|
||||||
|
|
||||||
|
|
||||||
def is_token_expired(expires_at: datetime | None) -> bool:
|
def is_token_expired(expires_at: datetime | None) -> bool:
|
||||||
@@ -81,4 +81,4 @@ def is_token_expired(expires_at: datetime | None) -> bool:
|
|||||||
"""
|
"""
|
||||||
if expires_at is None:
|
if expires_at is None:
|
||||||
return True
|
return True
|
||||||
return datetime.utcnow() > expires_at
|
return datetime.now(UTC) > expires_at
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""JWT token handling for cabinet authentication."""
|
"""JWT token handling for cabinet authentication."""
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
@@ -23,13 +23,13 @@ def create_access_token(user_id: int, telegram_id: int | None = None) -> str:
|
|||||||
Encoded JWT access token
|
Encoded JWT access token
|
||||||
"""
|
"""
|
||||||
expire_minutes = settings.get_cabinet_access_token_expire_minutes()
|
expire_minutes = settings.get_cabinet_access_token_expire_minutes()
|
||||||
expires = datetime.utcnow() + timedelta(minutes=expire_minutes)
|
expires = datetime.now(UTC) + timedelta(minutes=expire_minutes)
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
'sub': str(user_id),
|
'sub': str(user_id),
|
||||||
'type': 'access',
|
'type': 'access',
|
||||||
'exp': expires,
|
'exp': expires,
|
||||||
'iat': datetime.utcnow(),
|
'iat': datetime.now(UTC),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Добавляем telegram_id только если он есть
|
# Добавляем telegram_id только если он есть
|
||||||
@@ -51,13 +51,13 @@ def create_refresh_token(user_id: int) -> str:
|
|||||||
Encoded JWT refresh token
|
Encoded JWT refresh token
|
||||||
"""
|
"""
|
||||||
expire_days = settings.get_cabinet_refresh_token_expire_days()
|
expire_days = settings.get_cabinet_refresh_token_expire_days()
|
||||||
expires = datetime.utcnow() + timedelta(days=expire_days)
|
expires = datetime.now(UTC) + timedelta(days=expire_days)
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
'sub': str(user_id),
|
'sub': str(user_id),
|
||||||
'type': 'refresh',
|
'type': 'refresh',
|
||||||
'exp': expires,
|
'exp': expires,
|
||||||
'iat': datetime.utcnow(),
|
'iat': datetime.now(UTC),
|
||||||
}
|
}
|
||||||
|
|
||||||
secret = settings.get_cabinet_jwt_secret()
|
secret = settings.get_cabinet_jwt_secret()
|
||||||
@@ -108,4 +108,4 @@ def get_token_payload(token: str, expected_type: str = 'access') -> dict[str, An
|
|||||||
def get_refresh_token_expires_at() -> datetime:
|
def get_refresh_token_expires_at() -> datetime:
|
||||||
"""Get the expiration datetime for a new refresh token."""
|
"""Get the expiration datetime for a new refresh token."""
|
||||||
expire_days = settings.get_cabinet_refresh_token_expire_days()
|
expire_days = settings.get_cabinet_refresh_token_expire_days()
|
||||||
return datetime.utcnow() + timedelta(days=expire_days)
|
return datetime.now(UTC) + timedelta(days=expire_days)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
import json
|
import json
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from urllib.parse import parse_qsl, unquote
|
from urllib.parse import parse_qsl, unquote
|
||||||
|
|
||||||
@@ -34,8 +34,8 @@ def validate_telegram_login_widget(data: dict[str, Any], max_age_seconds: int =
|
|||||||
if auth_date:
|
if auth_date:
|
||||||
try:
|
try:
|
||||||
# Use UTC timestamp to avoid timezone issues
|
# Use UTC timestamp to avoid timezone issues
|
||||||
auth_time = datetime.utcfromtimestamp(int(auth_date))
|
auth_time = datetime.fromtimestamp(int(auth_date), tz=UTC)
|
||||||
age = (datetime.utcnow() - auth_time).total_seconds()
|
age = (datetime.now(UTC) - auth_time).total_seconds()
|
||||||
if age > max_age_seconds:
|
if age > max_age_seconds:
|
||||||
return False
|
return False
|
||||||
except (ValueError, TypeError, OSError):
|
except (ValueError, TypeError, OSError):
|
||||||
@@ -81,8 +81,8 @@ def validate_telegram_init_data(init_data: str, max_age_seconds: int = 86400) ->
|
|||||||
if auth_date:
|
if auth_date:
|
||||||
try:
|
try:
|
||||||
# Use UTC timestamp to avoid timezone issues
|
# Use UTC timestamp to avoid timezone issues
|
||||||
auth_time = datetime.utcfromtimestamp(int(auth_date))
|
auth_time = datetime.fromtimestamp(int(auth_date), tz=UTC)
|
||||||
age = (datetime.utcnow() - auth_time).total_seconds()
|
age = (datetime.now(UTC) - auth_time).total_seconds()
|
||||||
if age > max_age_seconds:
|
if age > max_age_seconds:
|
||||||
return None
|
return None
|
||||||
except (ValueError, TypeError, OSError):
|
except (ValueError, TypeError, OSError):
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Admin routes for broadcasts in cabinet."""
|
"""Admin routes for broadcasts in cabinet."""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||||
@@ -724,7 +724,7 @@ async def stop_broadcast(
|
|||||||
broadcast.status = 'cancelling'
|
broadcast.status = 'cancelling'
|
||||||
else:
|
else:
|
||||||
broadcast.status = 'cancelled'
|
broadcast.status = 'cancelled'
|
||||||
broadcast.completed_at = datetime.utcnow()
|
broadcast.completed_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(broadcast)
|
await db.refresh(broadcast)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Admin routes for pinned messages in cabinet."""
|
"""Admin routes for pinned messages in cabinet."""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from aiogram import Bot
|
from aiogram import Bot
|
||||||
@@ -227,7 +227,7 @@ async def update_pinned_message(
|
|||||||
if payload.send_on_every_start is not None:
|
if payload.send_on_every_start is not None:
|
||||||
msg.send_on_every_start = payload.send_on_every_start
|
msg.send_on_every_start = payload.send_on_every_start
|
||||||
|
|
||||||
msg.updated_at = datetime.utcnow()
|
msg.updated_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(msg)
|
await db.refresh(msg)
|
||||||
|
|
||||||
@@ -255,7 +255,7 @@ async def update_pinned_message_settings(
|
|||||||
if payload.send_on_every_start is not None:
|
if payload.send_on_every_start is not None:
|
||||||
msg.send_on_every_start = payload.send_on_every_start
|
msg.send_on_every_start = payload.send_on_every_start
|
||||||
|
|
||||||
msg.updated_at = datetime.utcnow()
|
msg.updated_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(msg)
|
await db.refresh(msg)
|
||||||
|
|
||||||
@@ -332,11 +332,11 @@ async def activate_pinned_message(
|
|||||||
await db.execute(
|
await db.execute(
|
||||||
update(PinnedMessage)
|
update(PinnedMessage)
|
||||||
.where(PinnedMessage.is_active.is_(True))
|
.where(PinnedMessage.is_active.is_(True))
|
||||||
.values(is_active=False, updated_at=datetime.utcnow())
|
.values(is_active=False, updated_at=datetime.now(UTC))
|
||||||
)
|
)
|
||||||
|
|
||||||
msg.is_active = True
|
msg.is_active = True
|
||||||
msg.updated_at = datetime.utcnow()
|
msg.updated_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(msg)
|
await db.refresh(msg)
|
||||||
|
|
||||||
|
|||||||
@@ -162,9 +162,9 @@ def _normalize_datetime(value: datetime | None) -> datetime | None:
|
|||||||
if value is None:
|
if value is None:
|
||||||
return None
|
return None
|
||||||
if value.tzinfo is not None and value.utcoffset() is not None:
|
if value.tzinfo is not None and value.utcoffset() is not None:
|
||||||
return value.astimezone(UTC).replace(tzinfo=None)
|
return value.astimezone(UTC)
|
||||||
if value.tzinfo is not None:
|
if value.tzinfo is not None:
|
||||||
return value.replace(tzinfo=None)
|
return value
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Admin routes for RemnaWave management in cabinet."""
|
"""Admin routes for RemnaWave management in cabinet."""
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
@@ -338,7 +338,7 @@ async def get_node_usage(
|
|||||||
service = _get_service()
|
service = _get_service()
|
||||||
_ensure_configured(service)
|
_ensure_configured(service)
|
||||||
|
|
||||||
end_dt = end or datetime.utcnow()
|
end_dt = end or datetime.now(UTC)
|
||||||
start_dt = start or (end_dt - timedelta(days=7))
|
start_dt = start or (end_dt - timedelta(days=7))
|
||||||
|
|
||||||
if start_dt >= end_dt:
|
if start_dt >= end_dt:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
@@ -258,7 +258,7 @@ async def get_dashboard_stats(
|
|||||||
sub_stats = await get_subscriptions_statistics(db)
|
sub_stats = await get_subscriptions_statistics(db)
|
||||||
|
|
||||||
# Get financial statistics
|
# Get financial statistics
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
|
||||||
trans_stats = await get_transactions_statistics(db, month_start, now)
|
trans_stats = await get_transactions_statistics(db, month_start, now)
|
||||||
@@ -503,7 +503,7 @@ async def _get_tariff_stats(db: AsyncSession) -> TariffStats | None:
|
|||||||
logger.info('📊 Нет тарифов в системе, пропускаем статистику')
|
logger.info('📊 Нет тарифов в системе, пропускаем статистику')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
week_ago = now - timedelta(days=7)
|
week_ago = now - timedelta(days=7)
|
||||||
month_ago = now - timedelta(days=30)
|
month_ago = now - timedelta(days=30)
|
||||||
@@ -601,7 +601,7 @@ async def get_top_referrers(
|
|||||||
):
|
):
|
||||||
"""Get top referrers with earnings breakdown by period."""
|
"""Get top referrers with earnings breakdown by period."""
|
||||||
try:
|
try:
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
week_ago = now - timedelta(days=7)
|
week_ago = now - timedelta(days=7)
|
||||||
month_ago = now - timedelta(days=30)
|
month_ago = now - timedelta(days=30)
|
||||||
@@ -874,7 +874,7 @@ async def get_recent_payments(
|
|||||||
):
|
):
|
||||||
"""Get recent payments with user info."""
|
"""Get recent payments with user info."""
|
||||||
try:
|
try:
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
week_ago = now - timedelta(days=7)
|
week_ago = now - timedelta(days=7)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Admin tickets routes for cabinet."""
|
"""Admin tickets routes for cabinet."""
|
||||||
|
|
||||||
import math
|
import math
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||||
@@ -447,13 +447,13 @@ async def reply_to_ticket(
|
|||||||
user_id=ticket.user_id,
|
user_id=ticket.user_id,
|
||||||
message_text=request.message,
|
message_text=request.message,
|
||||||
is_from_admin=True,
|
is_from_admin=True,
|
||||||
created_at=datetime.utcnow(),
|
created_at=datetime.now(UTC),
|
||||||
)
|
)
|
||||||
db.add(message)
|
db.add(message)
|
||||||
|
|
||||||
# Update ticket status to answered
|
# Update ticket status to answered
|
||||||
ticket.status = 'answered'
|
ticket.status = 'answered'
|
||||||
ticket.updated_at = datetime.utcnow()
|
ticket.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(message)
|
await db.refresh(message)
|
||||||
@@ -522,9 +522,9 @@ async def update_ticket_status(
|
|||||||
)
|
)
|
||||||
|
|
||||||
ticket.status = request.status
|
ticket.status = request.status
|
||||||
ticket.updated_at = datetime.utcnow()
|
ticket.updated_at = datetime.now(UTC)
|
||||||
if request.status == 'closed':
|
if request.status == 'closed':
|
||||||
ticket.closed_at = datetime.utcnow()
|
ticket.closed_at = datetime.now(UTC)
|
||||||
else:
|
else:
|
||||||
ticket.closed_at = None
|
ticket.closed_at = None
|
||||||
|
|
||||||
@@ -581,7 +581,7 @@ async def update_ticket_priority(
|
|||||||
)
|
)
|
||||||
|
|
||||||
ticket.priority = request.priority
|
ticket.priority = request.priority
|
||||||
ticket.updated_at = datetime.utcnow()
|
ticket.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(ticket)
|
await db.refresh(ticket)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Admin routes for version and release information."""
|
"""Admin routes for version and release information."""
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import structlog
|
import structlog
|
||||||
@@ -53,7 +53,7 @@ async def _fetch_cabinet_releases(force: bool = False) -> list[dict]:
|
|||||||
global _cabinet_last_check
|
global _cabinet_last_check
|
||||||
|
|
||||||
if not force and _cabinet_cache.get('releases') and _cabinet_last_check:
|
if not force and _cabinet_cache.get('releases') and _cabinet_last_check:
|
||||||
if datetime.now() - _cabinet_last_check < timedelta(seconds=_CACHE_TTL):
|
if datetime.now(UTC) - _cabinet_last_check < timedelta(seconds=_CACHE_TTL):
|
||||||
return _cabinet_cache['releases']
|
return _cabinet_cache['releases']
|
||||||
|
|
||||||
url = f'https://api.github.com/repos/{CABINET_REPO}/releases'
|
url = f'https://api.github.com/repos/{CABINET_REPO}/releases'
|
||||||
@@ -75,7 +75,7 @@ async def _fetch_cabinet_releases(force: bool = False) -> list[dict]:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
_cabinet_cache['releases'] = releases
|
_cabinet_cache['releases'] = releases
|
||||||
_cabinet_last_check = datetime.now()
|
_cabinet_last_check = datetime.now(UTC)
|
||||||
logger.info('Fetched cabinet releases from GitHub', releases_count=len(releases))
|
logger.info('Fetched cabinet releases from GitHub', releases_count=len(releases))
|
||||||
return releases
|
return releases
|
||||||
logger.warning('GitHub API returned status for cabinet releases', response_status=response.status)
|
logger.warning('GitHub API returned status for cabinet releases', response_status=response.status)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Admin routes for managing users in cabinet."""
|
"""Admin routes for managing users in cabinet."""
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||||
@@ -35,7 +35,7 @@ from app.database.models import (
|
|||||||
User,
|
User,
|
||||||
UserStatus,
|
UserStatus,
|
||||||
)
|
)
|
||||||
from app.utils.timezone import panel_datetime_to_naive_utc
|
from app.utils.timezone import panel_datetime_to_utc
|
||||||
|
|
||||||
from ..dependencies import get_cabinet_db, get_current_admin_user
|
from ..dependencies import get_cabinet_db, get_current_admin_user
|
||||||
from ..schemas.users import (
|
from ..schemas.users import (
|
||||||
@@ -144,9 +144,9 @@ def _build_subscription_info(subscription: Subscription, tariff_name: str | None
|
|||||||
is_active = False
|
is_active = False
|
||||||
|
|
||||||
if subscription.end_date:
|
if subscription.end_date:
|
||||||
delta = subscription.end_date - datetime.utcnow()
|
delta = subscription.end_date - datetime.now(UTC)
|
||||||
days_remaining = max(0, delta.days)
|
days_remaining = max(0, delta.days)
|
||||||
is_active = subscription.status == SubscriptionStatus.ACTIVE.value and subscription.end_date > datetime.utcnow()
|
is_active = subscription.status == SubscriptionStatus.ACTIVE.value and subscription.end_date > datetime.now(UTC)
|
||||||
|
|
||||||
return UserSubscriptionInfo(
|
return UserSubscriptionInfo(
|
||||||
id=subscription.id,
|
id=subscription.id,
|
||||||
@@ -174,7 +174,7 @@ async def _build_subscription_info_async(db: AsyncSession, subscription: Subscri
|
|||||||
tariff_name = tariff.name
|
tariff_name = tariff.name
|
||||||
|
|
||||||
# Fetch traffic purchases
|
# Fetch traffic purchases
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
tp_query = (
|
tp_query = (
|
||||||
select(TrafficPurchase)
|
select(TrafficPurchase)
|
||||||
.where(TrafficPurchase.subscription_id == subscription.id)
|
.where(TrafficPurchase.subscription_id == subscription.id)
|
||||||
@@ -225,13 +225,13 @@ async def _sync_subscription_to_panel(db: AsyncSession, user: User, subscription
|
|||||||
is_active = (
|
is_active = (
|
||||||
subscription.status in (SubscriptionStatus.ACTIVE.value, SubscriptionStatus.TRIAL.value)
|
subscription.status in (SubscriptionStatus.ACTIVE.value, SubscriptionStatus.TRIAL.value)
|
||||||
and subscription.end_date
|
and subscription.end_date
|
||||||
and subscription.end_date > datetime.utcnow()
|
and subscription.end_date > datetime.now(UTC)
|
||||||
)
|
)
|
||||||
panel_status = PanelUserStatus.ACTIVE if is_active else PanelUserStatus.DISABLED
|
panel_status = PanelUserStatus.ACTIVE if is_active else PanelUserStatus.DISABLED
|
||||||
|
|
||||||
expire_at = subscription.end_date
|
expire_at = subscription.end_date
|
||||||
if expire_at and expire_at <= datetime.utcnow():
|
if expire_at and expire_at <= datetime.now(UTC):
|
||||||
expire_at = datetime.utcnow() + timedelta(minutes=1)
|
expire_at = datetime.now(UTC) + timedelta(minutes=1)
|
||||||
|
|
||||||
username = settings.format_remnawave_username(
|
username = settings.format_remnawave_username(
|
||||||
full_name=user.full_name,
|
full_name=user.full_name,
|
||||||
@@ -310,7 +310,7 @@ async def _sync_subscription_to_panel(db: AsyncSession, user: User, subscription
|
|||||||
# Create new user
|
# Create new user
|
||||||
create_kwargs = {
|
create_kwargs = {
|
||||||
'username': username,
|
'username': username,
|
||||||
'expire_at': expire_at or (datetime.utcnow() + timedelta(days=30)),
|
'expire_at': expire_at or (datetime.now(UTC) + timedelta(days=30)),
|
||||||
'status': panel_status,
|
'status': panel_status,
|
||||||
'traffic_limit_bytes': traffic_limit_bytes,
|
'traffic_limit_bytes': traffic_limit_bytes,
|
||||||
'traffic_limit_strategy': TrafficLimitStrategy.MONTH,
|
'traffic_limit_strategy': TrafficLimitStrategy.MONTH,
|
||||||
@@ -330,7 +330,7 @@ async def _sync_subscription_to_panel(db: AsyncSession, user: User, subscription
|
|||||||
changes['panel_uuid'] = new_panel_user.uuid
|
changes['panel_uuid'] = new_panel_user.uuid
|
||||||
logger.info('Created user in Remnawave panel', user_id=user.id, uuid=new_panel_user.uuid)
|
logger.info('Created user in Remnawave panel', user_id=user.id, uuid=new_panel_user.uuid)
|
||||||
|
|
||||||
user.last_remnawave_sync = datetime.utcnow()
|
user.last_remnawave_sync = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
return changes
|
return changes
|
||||||
@@ -421,7 +421,7 @@ async def get_users_stats(
|
|||||||
func.cast(
|
func.cast(
|
||||||
and_(
|
and_(
|
||||||
Subscription.status == SubscriptionStatus.ACTIVE.value,
|
Subscription.status == SubscriptionStatus.ACTIVE.value,
|
||||||
Subscription.end_date > datetime.utcnow(),
|
Subscription.end_date > datetime.now(UTC),
|
||||||
),
|
),
|
||||||
Integer,
|
Integer,
|
||||||
)
|
)
|
||||||
@@ -431,7 +431,7 @@ async def get_users_stats(
|
|||||||
func.cast(
|
func.cast(
|
||||||
or_(
|
or_(
|
||||||
Subscription.status == SubscriptionStatus.EXPIRED.value,
|
Subscription.status == SubscriptionStatus.EXPIRED.value,
|
||||||
Subscription.end_date <= datetime.utcnow(),
|
Subscription.end_date <= datetime.now(UTC),
|
||||||
),
|
),
|
||||||
Integer,
|
Integer,
|
||||||
)
|
)
|
||||||
@@ -456,7 +456,7 @@ async def get_users_stats(
|
|||||||
avg_balance = int(balance_row.avg or 0) if balance_row else 0
|
avg_balance = int(balance_row.avg or 0) if balance_row else 0
|
||||||
|
|
||||||
# Get activity stats
|
# Get activity stats
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
week_ago = now - timedelta(days=7)
|
week_ago = now - timedelta(days=7)
|
||||||
month_ago = now - timedelta(days=30)
|
month_ago = now - timedelta(days=30)
|
||||||
@@ -756,7 +756,7 @@ async def get_user_node_usage(
|
|||||||
if not service.is_configured:
|
if not service.is_configured:
|
||||||
return UserNodeUsageResponse(items=[])
|
return UserNodeUsageResponse(items=[])
|
||||||
|
|
||||||
end_date = datetime.utcnow()
|
end_date = datetime.now(UTC)
|
||||||
start_date = end_date - timedelta(days=30)
|
start_date = end_date - timedelta(days=30)
|
||||||
start_str = start_date.strftime('%Y-%m-%d')
|
start_str = start_date.strftime('%Y-%m-%d')
|
||||||
end_str = end_date.strftime('%Y-%m-%d')
|
end_str = end_date.strftime('%Y-%m-%d')
|
||||||
@@ -1011,7 +1011,7 @@ async def update_user_subscription(
|
|||||||
)
|
)
|
||||||
|
|
||||||
subscription.end_date = request.end_date
|
subscription.end_date = request.end_date
|
||||||
if request.end_date > datetime.utcnow():
|
if request.end_date > datetime.now(UTC):
|
||||||
subscription.status = SubscriptionStatus.ACTIVE.value
|
subscription.status = SubscriptionStatus.ACTIVE.value
|
||||||
else:
|
else:
|
||||||
subscription.status = SubscriptionStatus.EXPIRED.value
|
subscription.status = SubscriptionStatus.EXPIRED.value
|
||||||
@@ -1107,7 +1107,7 @@ async def update_user_subscription(
|
|||||||
|
|
||||||
if request.action == 'cancel':
|
if request.action == 'cancel':
|
||||||
subscription.status = SubscriptionStatus.EXPIRED.value
|
subscription.status = SubscriptionStatus.EXPIRED.value
|
||||||
subscription.end_date = datetime.utcnow()
|
subscription.end_date = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(subscription)
|
await db.refresh(subscription)
|
||||||
|
|
||||||
@@ -1124,9 +1124,9 @@ async def update_user_subscription(
|
|||||||
|
|
||||||
if request.action == 'activate':
|
if request.action == 'activate':
|
||||||
subscription.status = SubscriptionStatus.ACTIVE.value
|
subscription.status = SubscriptionStatus.ACTIVE.value
|
||||||
if subscription.end_date and subscription.end_date <= datetime.utcnow():
|
if subscription.end_date and subscription.end_date <= datetime.now(UTC):
|
||||||
# Extend by 30 days if expired
|
# Extend by 30 days if expired
|
||||||
subscription.end_date = datetime.utcnow() + timedelta(days=30)
|
subscription.end_date = datetime.now(UTC) + timedelta(days=30)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(subscription)
|
await db.refresh(subscription)
|
||||||
|
|
||||||
@@ -1196,7 +1196,7 @@ async def update_user_subscription(
|
|||||||
await db.delete(traffic_purchase)
|
await db.delete(traffic_purchase)
|
||||||
|
|
||||||
# Recalculate traffic_reset_at from remaining active purchases
|
# Recalculate traffic_reset_at from remaining active purchases
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
remaining_query = select(TrafficPurchase).where(
|
remaining_query = select(TrafficPurchase).where(
|
||||||
TrafficPurchase.subscription_id == subscription.id,
|
TrafficPurchase.subscription_id == subscription.id,
|
||||||
TrafficPurchase.expires_at > now,
|
TrafficPurchase.expires_at > now,
|
||||||
@@ -1388,7 +1388,7 @@ async def update_user_status(
|
|||||||
)
|
)
|
||||||
|
|
||||||
user.status = new_status
|
user.status = new_status
|
||||||
user.updated_at = datetime.utcnow()
|
user.updated_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(user)
|
await db.refresh(user)
|
||||||
|
|
||||||
@@ -1456,7 +1456,7 @@ async def update_user_restrictions(
|
|||||||
if request.restriction_reason is not None:
|
if request.restriction_reason is not None:
|
||||||
user.restriction_reason = request.restriction_reason
|
user.restriction_reason = request.restriction_reason
|
||||||
|
|
||||||
user.updated_at = datetime.utcnow()
|
user.updated_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(user)
|
await db.refresh(user)
|
||||||
|
|
||||||
@@ -1511,7 +1511,7 @@ async def update_user_promo_group(
|
|||||||
promo_group_name = promo_group.name
|
promo_group_name = promo_group.name
|
||||||
|
|
||||||
user.promo_group_id = new_promo_group_id
|
user.promo_group_id = new_promo_group_id
|
||||||
user.updated_at = datetime.utcnow()
|
user.updated_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(user)
|
await db.refresh(user)
|
||||||
|
|
||||||
@@ -1552,7 +1552,7 @@ async def update_user_referral_commission(
|
|||||||
|
|
||||||
old_commission = user.referral_commission_percent
|
old_commission = user.referral_commission_percent
|
||||||
user.referral_commission_percent = request.commission_percent
|
user.referral_commission_percent = request.commission_percent
|
||||||
user.updated_at = datetime.utcnow()
|
user.updated_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -1837,7 +1837,7 @@ async def reset_user_trial(
|
|||||||
|
|
||||||
# Reset trial flag
|
# Reset trial flag
|
||||||
user.has_used_trial = False
|
user.has_used_trial = False
|
||||||
user.updated_at = datetime.utcnow()
|
user.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
@@ -1907,7 +1907,7 @@ async def reset_user_subscription(
|
|||||||
await db.execute(delete(Subscription).where(Subscription.user_id == user_id))
|
await db.execute(delete(Subscription).where(Subscription.user_id == user_id))
|
||||||
subscription_deleted = True
|
subscription_deleted = True
|
||||||
|
|
||||||
user.updated_at = datetime.utcnow()
|
user.updated_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
reason_text = f' (reason: {request.reason})' if request.reason else ''
|
reason_text = f' (reason: {request.reason})' if request.reason else ''
|
||||||
@@ -1971,7 +1971,7 @@ async def disable_user(
|
|||||||
|
|
||||||
# Block user account
|
# Block user account
|
||||||
user.status = UserStatus.BLOCKED.value
|
user.status = UserStatus.BLOCKED.value
|
||||||
user.updated_at = datetime.utcnow()
|
user.updated_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
reason_text = f' (reason: {request.reason})' if request.reason else ''
|
reason_text = f' (reason: {request.reason})' if request.reason else ''
|
||||||
@@ -2179,13 +2179,8 @@ async def get_user_sync_status(
|
|||||||
differences.append(f'Status: bot={bot_sub_status}, panel={panel_status}')
|
differences.append(f'Status: bot={bot_sub_status}, panel={panel_status}')
|
||||||
|
|
||||||
if bot_sub_end_date and panel_expire_at:
|
if bot_sub_end_date and panel_expire_at:
|
||||||
# Convert both to naive UTC for comparison
|
bot_end_utc = bot_sub_end_date if bot_sub_end_date.tzinfo else bot_sub_end_date
|
||||||
# Bot dates are stored as naive UTC
|
panel_end_utc = panel_datetime_to_utc(panel_expire_at)
|
||||||
bot_end_utc = (
|
|
||||||
bot_sub_end_date.replace(tzinfo=None) if bot_sub_end_date.tzinfo else bot_sub_end_date
|
|
||||||
)
|
|
||||||
# Panel returns local time with misleading +00:00 offset
|
|
||||||
panel_end_utc = panel_datetime_to_naive_utc(panel_expire_at)
|
|
||||||
|
|
||||||
diff_seconds = abs((bot_end_utc - panel_end_utc).total_seconds())
|
diff_seconds = abs((bot_end_utc - panel_end_utc).total_seconds())
|
||||||
# Allow for timezone offset (3 hours = MSK) and small sync delays
|
# Allow for timezone offset (3 hours = MSK) and small sync delays
|
||||||
@@ -2319,7 +2314,7 @@ async def sync_user_from_panel(
|
|||||||
short_uuid=panel_user.short_uuid,
|
short_uuid=panel_user.short_uuid,
|
||||||
username=panel_user.username,
|
username=panel_user.username,
|
||||||
status=panel_user.status.value if panel_user.status else None,
|
status=panel_user.status.value if panel_user.status else None,
|
||||||
expire_at=panel_datetime_to_naive_utc(panel_user.expire_at) if panel_user.expire_at else None,
|
expire_at=panel_datetime_to_utc(panel_user.expire_at) if panel_user.expire_at else None,
|
||||||
traffic_limit_gb=panel_user.traffic_limit_bytes / (1024**3) if panel_user.traffic_limit_bytes else 0,
|
traffic_limit_gb=panel_user.traffic_limit_bytes / (1024**3) if panel_user.traffic_limit_bytes else 0,
|
||||||
traffic_used_gb=panel_user.used_traffic_bytes / (1024**3) if panel_user.used_traffic_bytes else 0,
|
traffic_used_gb=panel_user.used_traffic_bytes / (1024**3) if panel_user.used_traffic_bytes else 0,
|
||||||
device_limit=panel_user.hwid_device_limit or 1,
|
device_limit=panel_user.hwid_device_limit or 1,
|
||||||
@@ -2338,13 +2333,10 @@ async def sync_user_from_panel(
|
|||||||
|
|
||||||
# Update end date (normalize timezone)
|
# Update end date (normalize timezone)
|
||||||
if panel_user.expire_at:
|
if panel_user.expire_at:
|
||||||
# Panel returns local time with misleading +00:00 offset
|
panel_expire_utc = panel_datetime_to_utc(panel_user.expire_at)
|
||||||
panel_expire_utc = panel_datetime_to_naive_utc(panel_user.expire_at)
|
|
||||||
|
|
||||||
sub_end_naive = (
|
sub_end_utc = sub.end_date if sub.end_date and sub.end_date.tzinfo else sub.end_date
|
||||||
sub.end_date.replace(tzinfo=None) if sub.end_date and sub.end_date.tzinfo else sub.end_date
|
if sub_end_utc != panel_expire_utc:
|
||||||
)
|
|
||||||
if sub_end_naive != panel_expire_utc:
|
|
||||||
changes['end_date'] = {
|
changes['end_date'] = {
|
||||||
'old': sub.end_date.isoformat() if sub.end_date else None,
|
'old': sub.end_date.isoformat() if sub.end_date else None,
|
||||||
'new': panel_expire_utc.isoformat(),
|
'new': panel_expire_utc.isoformat(),
|
||||||
@@ -2353,7 +2345,7 @@ async def sync_user_from_panel(
|
|||||||
|
|
||||||
# Update status
|
# Update status
|
||||||
panel_status_str = panel_user.status.value if panel_user.status else 'DISABLED'
|
panel_status_str = panel_user.status.value if panel_user.status else 'DISABLED'
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
# Compare with normalized panel expire date
|
# Compare with normalized panel expire date
|
||||||
panel_expire_for_check = panel_expire_utc if panel_user.expire_at else None
|
panel_expire_for_check = panel_expire_utc if panel_user.expire_at else None
|
||||||
if panel_status_str == 'ACTIVE' and panel_expire_for_check and panel_expire_for_check > now:
|
if panel_status_str == 'ACTIVE' and panel_expire_for_check and panel_expire_for_check > now:
|
||||||
@@ -2410,9 +2402,8 @@ async def sync_user_from_panel(
|
|||||||
panel_traffic_limit = (
|
panel_traffic_limit = (
|
||||||
int(panel_user.traffic_limit_bytes / (1024**3)) if panel_user.traffic_limit_bytes else 100
|
int(panel_user.traffic_limit_bytes / (1024**3)) if panel_user.traffic_limit_bytes else 100
|
||||||
)
|
)
|
||||||
# Panel returns local time with misleading +00:00 offset
|
panel_expire_utc = panel_datetime_to_utc(panel_user.expire_at)
|
||||||
panel_expire_naive = panel_datetime_to_naive_utc(panel_user.expire_at)
|
days_remaining = max(1, (panel_expire_utc - datetime.now(UTC)).days)
|
||||||
days_remaining = max(1, (panel_expire_naive - datetime.utcnow()).days)
|
|
||||||
|
|
||||||
new_sub = await create_paid_subscription(
|
new_sub = await create_paid_subscription(
|
||||||
db=db,
|
db=db,
|
||||||
@@ -2427,8 +2418,8 @@ async def sync_user_from_panel(
|
|||||||
changes['subscription_created'] = True
|
changes['subscription_created'] = True
|
||||||
|
|
||||||
# Update last sync time
|
# Update last sync time
|
||||||
user.last_remnawave_sync = datetime.utcnow()
|
user.last_remnawave_sync = datetime.now(UTC)
|
||||||
user.updated_at = datetime.utcnow()
|
user.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
@@ -2502,14 +2493,14 @@ async def sync_user_to_panel(
|
|||||||
is_active = (
|
is_active = (
|
||||||
sub.status in (SubscriptionStatus.ACTIVE.value, SubscriptionStatus.TRIAL.value)
|
sub.status in (SubscriptionStatus.ACTIVE.value, SubscriptionStatus.TRIAL.value)
|
||||||
and sub.end_date
|
and sub.end_date
|
||||||
and sub.end_date > datetime.utcnow()
|
and sub.end_date > datetime.now(UTC)
|
||||||
)
|
)
|
||||||
panel_status = PanelUserStatus.ACTIVE if is_active else PanelUserStatus.DISABLED
|
panel_status = PanelUserStatus.ACTIVE if is_active else PanelUserStatus.DISABLED
|
||||||
|
|
||||||
# Ensure expire_at is in future for panel
|
# Ensure expire_at is in future for panel
|
||||||
expire_at = sub.end_date
|
expire_at = sub.end_date
|
||||||
if expire_at and expire_at <= datetime.utcnow():
|
if expire_at and expire_at <= datetime.now(UTC):
|
||||||
expire_at = datetime.utcnow() + timedelta(minutes=1)
|
expire_at = datetime.now(UTC) + timedelta(minutes=1)
|
||||||
|
|
||||||
username = settings.format_remnawave_username(
|
username = settings.format_remnawave_username(
|
||||||
full_name=user.full_name,
|
full_name=user.full_name,
|
||||||
@@ -2595,7 +2586,7 @@ async def sync_user_to_panel(
|
|||||||
# Create new user in panel
|
# Create new user in panel
|
||||||
create_kwargs = {
|
create_kwargs = {
|
||||||
'username': username,
|
'username': username,
|
||||||
'expire_at': expire_at or (datetime.utcnow() + timedelta(days=30)),
|
'expire_at': expire_at or (datetime.now(UTC) + timedelta(days=30)),
|
||||||
'status': panel_status,
|
'status': panel_status,
|
||||||
'traffic_limit_bytes': traffic_limit_bytes,
|
'traffic_limit_bytes': traffic_limit_bytes,
|
||||||
'traffic_limit_strategy': TrafficLimitStrategy.MONTH,
|
'traffic_limit_strategy': TrafficLimitStrategy.MONTH,
|
||||||
@@ -2620,8 +2611,8 @@ async def sync_user_to_panel(
|
|||||||
action = 'created'
|
action = 'created'
|
||||||
|
|
||||||
# Update last sync time
|
# Update last sync time
|
||||||
user.last_remnawave_sync = datetime.utcnow()
|
user.last_remnawave_sync = datetime.now(UTC)
|
||||||
user.updated_at = datetime.utcnow()
|
user.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
|
|||||||
+11
-11
@@ -25,7 +25,7 @@ from app.database.crud.user import (
|
|||||||
from app.database.models import CabinetRefreshToken, User
|
from app.database.models import CabinetRefreshToken, User
|
||||||
from app.services.disposable_email_service import disposable_email_service
|
from app.services.disposable_email_service import disposable_email_service
|
||||||
from app.services.referral_service import process_referral_registration
|
from app.services.referral_service import process_referral_registration
|
||||||
from app.utils.timezone import panel_datetime_to_naive_utc
|
from app.utils.timezone import panel_datetime_to_utc
|
||||||
|
|
||||||
from ..auth import (
|
from ..auth import (
|
||||||
create_access_token,
|
create_access_token,
|
||||||
@@ -175,7 +175,7 @@ async def _sync_subscription_from_panel_by_email(db: AsyncSession, user: User) -
|
|||||||
existing_sub = await get_subscription_by_user_id(db, user.id)
|
existing_sub = await get_subscription_by_user_id(db, user.id)
|
||||||
|
|
||||||
# Parse panel data — panel returns local time with misleading +00:00 offset
|
# Parse panel data — panel returns local time with misleading +00:00 offset
|
||||||
expire_at = panel_datetime_to_naive_utc(panel_user.expire_at)
|
expire_at = panel_datetime_to_utc(panel_user.expire_at)
|
||||||
traffic_limit_gb = panel_user.traffic_limit_bytes // (1024**3) if panel_user.traffic_limit_bytes > 0 else 0
|
traffic_limit_gb = panel_user.traffic_limit_bytes // (1024**3) if panel_user.traffic_limit_bytes > 0 else 0
|
||||||
traffic_used_gb = panel_user.used_traffic_bytes / (1024**3) if panel_user.used_traffic_bytes > 0 else 0
|
traffic_used_gb = panel_user.used_traffic_bytes / (1024**3) if panel_user.used_traffic_bytes > 0 else 0
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ async def _sync_subscription_from_panel_by_email(db: AsyncSession, user: User) -
|
|||||||
device_limit = panel_user.hwid_device_limit or 1
|
device_limit = panel_user.hwid_device_limit or 1
|
||||||
|
|
||||||
# Determine status — expire_at is now naive UTC
|
# Determine status — expire_at is now naive UTC
|
||||||
current_time = datetime.now(UTC).replace(tzinfo=None)
|
current_time = datetime.now(UTC)
|
||||||
|
|
||||||
if panel_user.status.value == 'ACTIVE' and expire_at > current_time:
|
if panel_user.status.value == 'ACTIVE' and expire_at > current_time:
|
||||||
sub_status = SubscriptionStatus.ACTIVE
|
sub_status = SubscriptionStatus.ACTIVE
|
||||||
@@ -313,7 +313,7 @@ async def auth_telegram(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Update last login
|
# Update last login
|
||||||
user.cabinet_last_login = datetime.utcnow()
|
user.cabinet_last_login = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
response = _create_auth_response(user)
|
response = _create_auth_response(user)
|
||||||
@@ -374,7 +374,7 @@ async def auth_telegram_widget(
|
|||||||
if request.last_name != user.last_name:
|
if request.last_name != user.last_name:
|
||||||
user.last_name = request.last_name
|
user.last_name = request.last_name
|
||||||
|
|
||||||
user.cabinet_last_login = datetime.utcnow()
|
user.cabinet_last_login = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
response = _create_auth_response(user)
|
response = _create_auth_response(user)
|
||||||
@@ -546,7 +546,7 @@ async def register_email_standalone(
|
|||||||
# Для тестового email - автоматически верифицировать
|
# Для тестового email - автоматически верифицировать
|
||||||
if is_test_email:
|
if is_test_email:
|
||||||
user.email_verified = True
|
user.email_verified = True
|
||||||
user.email_verified_at = datetime.utcnow()
|
user.email_verified_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
logger.info('Test email auto-verified: user_id', email=request.email, user_id=user.id)
|
logger.info('Test email auto-verified: user_id', email=request.email, user_id=user.id)
|
||||||
else:
|
else:
|
||||||
@@ -633,10 +633,10 @@ async def verify_email(
|
|||||||
|
|
||||||
# Mark email as verified
|
# Mark email as verified
|
||||||
user.email_verified = True
|
user.email_verified = True
|
||||||
user.email_verified_at = datetime.utcnow()
|
user.email_verified_at = datetime.now(UTC)
|
||||||
user.email_verification_token = None
|
user.email_verification_token = None
|
||||||
user.email_verification_expires = None
|
user.email_verification_expires = None
|
||||||
user.cabinet_last_login = datetime.utcnow()
|
user.cabinet_last_login = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
@@ -750,7 +750,7 @@ async def login_email(
|
|||||||
language='ru',
|
language='ru',
|
||||||
)
|
)
|
||||||
user.email_verified = True
|
user.email_verified = True
|
||||||
user.email_verified_at = datetime.utcnow()
|
user.email_verified_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -783,7 +783,7 @@ async def login_email(
|
|||||||
detail='User account is not active',
|
detail='User account is not active',
|
||||||
)
|
)
|
||||||
|
|
||||||
user.cabinet_last_login = datetime.utcnow()
|
user.cabinet_last_login = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
response = _create_auth_response(user)
|
response = _create_auth_response(user)
|
||||||
@@ -871,7 +871,7 @@ async def logout(
|
|||||||
token_record = result.scalar_one_or_none()
|
token_record = result.scalar_one_or_none()
|
||||||
|
|
||||||
if token_record:
|
if token_record:
|
||||||
token_record.revoked_at = datetime.utcnow()
|
token_record.revoked_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
return {'message': 'Logged out successfully'}
|
return {'message': 'Logged out successfully'}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Contests routes for cabinet - user participation in games/contests."""
|
"""Contests routes for cabinet - user participation in games/contests."""
|
||||||
|
|
||||||
import random
|
import random
|
||||||
from datetime import datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
@@ -102,7 +102,7 @@ async def _award_prize(db: AsyncSession, user_id: int, prize_type: str, prize_va
|
|||||||
return 'Error: subscription not found'
|
return 'Error: subscription not found'
|
||||||
|
|
||||||
subscription.end_date = subscription.end_date + timedelta(days=days)
|
subscription.end_date = subscription.end_date + timedelta(days=days)
|
||||||
subscription.updated_at = datetime.utcnow()
|
subscription.updated_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(subscription)
|
await db.refresh(subscription)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Notification settings routes for cabinet."""
|
"""Notification settings routes for cabinet."""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
@@ -112,7 +112,7 @@ async def update_notification_settings(
|
|||||||
user.notification_settings = {}
|
user.notification_settings = {}
|
||||||
|
|
||||||
user.notification_settings = new_settings
|
user.notification_settings = new_settings
|
||||||
user.updated_at = datetime.utcnow()
|
user.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(user)
|
await db.refresh(user)
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ router = APIRouter(prefix='/auth/oauth', tags=['Cabinet OAuth'])
|
|||||||
|
|
||||||
async def _finalize_oauth_login(db: AsyncSession, user: User, provider: str) -> AuthResponse:
|
async def _finalize_oauth_login(db: AsyncSession, user: User, provider: str) -> AuthResponse:
|
||||||
"""Update last login, create tokens, store refresh token."""
|
"""Update last login, create tokens, store refresh token."""
|
||||||
user.cabinet_last_login = datetime.now(UTC).replace(tzinfo=None)
|
user.cabinet_last_login = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
auth_response = _create_auth_response(user)
|
auth_response = _create_auth_response(user)
|
||||||
await _store_refresh_token(db, user.id, auth_response.refresh_token, device_info=f'oauth:{provider}')
|
await _store_refresh_token(db, user.id, auth_response.refresh_token, device_info=f'oauth:{provider}')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Polls routes for cabinet - user participation in polls/surveys."""
|
"""Polls routes for cabinet - user participation in polls/surveys."""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
@@ -247,7 +247,7 @@ async def start_poll(
|
|||||||
|
|
||||||
# Mark as started if not already
|
# Mark as started if not already
|
||||||
if not response.started_at:
|
if not response.started_at:
|
||||||
response.started_at = datetime.utcnow()
|
response.started_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
# Get next unanswered question
|
# Get next unanswered question
|
||||||
@@ -346,7 +346,7 @@ async def answer_question(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Poll completed
|
# Poll completed
|
||||||
response.completed_at = datetime.utcnow()
|
response.completed_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
# Award reward if any
|
# Award reward if any
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Promo offers routes for cabinet - personal discounts and offers."""
|
"""Promo offers routes for cabinet - personal discounts and offers."""
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
@@ -112,7 +112,7 @@ async def get_promo_offers(
|
|||||||
db: AsyncSession = Depends(get_cabinet_db),
|
db: AsyncSession = Depends(get_cabinet_db),
|
||||||
):
|
):
|
||||||
"""Get list of available promo offers for the user."""
|
"""Get list of available promo offers for the user."""
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
|
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(DiscountOffer)
|
select(DiscountOffer)
|
||||||
@@ -151,7 +151,7 @@ async def get_active_discount(
|
|||||||
expires_at = user.promo_offer_discount_expires_at
|
expires_at = user.promo_offer_discount_expires_at
|
||||||
source = user.promo_offer_discount_source
|
source = user.promo_offer_discount_source
|
||||||
|
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
is_active = discount_percent > 0 and (expires_at is None or expires_at > now)
|
is_active = discount_percent > 0 and (expires_at is None or expires_at > now)
|
||||||
|
|
||||||
return ActiveDiscountInfo(
|
return ActiveDiscountInfo(
|
||||||
@@ -284,7 +284,7 @@ async def claim_promo_offer(
|
|||||||
detail='Offer not found',
|
detail='Offer not found',
|
||||||
)
|
)
|
||||||
|
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
|
|
||||||
if offer.claimed_at is not None:
|
if offer.claimed_at is not None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -408,7 +408,7 @@ async def clear_active_discount(
|
|||||||
user.promo_offer_discount_percent = 0
|
user.promo_offer_discount_percent = 0
|
||||||
user.promo_offer_discount_source = None
|
user.promo_offer_discount_source = None
|
||||||
user.promo_offer_discount_expires_at = None
|
user.promo_offer_discount_expires_at = None
|
||||||
user.updated_at = datetime.utcnow()
|
user.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ def _subscription_to_response(
|
|||||||
traffic_purchases: list[dict[str, Any]] | None = None,
|
traffic_purchases: list[dict[str, Any]] | None = None,
|
||||||
) -> SubscriptionData:
|
) -> SubscriptionData:
|
||||||
"""Convert Subscription model to response."""
|
"""Convert Subscription model to response."""
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
|
|
||||||
# Use actual_status property for correct status (same as bot uses)
|
# Use actual_status property for correct status (same as bot uses)
|
||||||
actual_status = subscription.actual_status
|
actual_status = subscription.actual_status
|
||||||
@@ -276,7 +276,7 @@ async def get_subscription(
|
|||||||
traffic_purchases_data = []
|
traffic_purchases_data = []
|
||||||
from app.database.models import TrafficPurchase
|
from app.database.models import TrafficPurchase
|
||||||
|
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
purchases_query = (
|
purchases_query = (
|
||||||
select(TrafficPurchase)
|
select(TrafficPurchase)
|
||||||
.where(TrafficPurchase.subscription_id == fresh_user.subscription.id)
|
.where(TrafficPurchase.subscription_id == fresh_user.subscription.id)
|
||||||
@@ -515,7 +515,7 @@ async def renew_subscription(
|
|||||||
user.promo_offer_discount_expires_at = None
|
user.promo_offer_discount_expires_at = None
|
||||||
|
|
||||||
# Extend from end_date or now if expired
|
# Extend from end_date or now if expired
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
if user.subscription.end_date and user.subscription.end_date > now:
|
if user.subscription.end_date and user.subscription.end_date > now:
|
||||||
user.subscription.end_date = user.subscription.end_date + timedelta(days=request.period_days)
|
user.subscription.end_date = user.subscription.end_date + timedelta(days=request.period_days)
|
||||||
else:
|
else:
|
||||||
@@ -832,9 +832,9 @@ async def purchase_traffic(
|
|||||||
# Устанавливаем дату сброса трафика (только при первой докупке)
|
# Устанавливаем дату сброса трафика (только при первой докупке)
|
||||||
# При повторной докупке дата НЕ продлевается
|
# При повторной докупке дата НЕ продлевается
|
||||||
if not subscription.traffic_reset_at:
|
if not subscription.traffic_reset_at:
|
||||||
from datetime import timedelta
|
from datetime import UTC, timedelta
|
||||||
|
|
||||||
subscription.traffic_reset_at = datetime.utcnow() + timedelta(days=30)
|
subscription.traffic_reset_at = datetime.now(UTC) + timedelta(days=30)
|
||||||
logger.info(
|
logger.info(
|
||||||
'Set traffic_reset_at for subscription',
|
'Set traffic_reset_at for subscription',
|
||||||
subscription_id=subscription.id,
|
subscription_id=subscription.id,
|
||||||
@@ -1137,7 +1137,7 @@ async def get_trial_info(
|
|||||||
|
|
||||||
# Check if user already has an active subscription
|
# Check if user already has an active subscription
|
||||||
if user.subscription:
|
if user.subscription:
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
is_active = (
|
is_active = (
|
||||||
user.subscription.status == 'active' and user.subscription.end_date and user.subscription.end_date > now
|
user.subscription.status == 'active' and user.subscription.end_date and user.subscription.end_date > now
|
||||||
)
|
)
|
||||||
@@ -1194,7 +1194,7 @@ async def activate_trial(
|
|||||||
|
|
||||||
# Check if user already has an active subscription
|
# Check if user already has an active subscription
|
||||||
if user.subscription:
|
if user.subscription:
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
is_active = (
|
is_active = (
|
||||||
user.subscription.status == 'active' and user.subscription.end_date and user.subscription.end_date > now
|
user.subscription.status == 'active' and user.subscription.end_date and user.subscription.end_date > now
|
||||||
)
|
)
|
||||||
@@ -2010,7 +2010,7 @@ async def purchase_tariff(
|
|||||||
|
|
||||||
# For daily tariffs, set last_daily_charge_at
|
# For daily tariffs, set last_daily_charge_at
|
||||||
if is_daily_tariff:
|
if is_daily_tariff:
|
||||||
subscription.last_daily_charge_at = datetime.utcnow()
|
subscription.last_daily_charge_at = datetime.now(UTC)
|
||||||
subscription.is_daily_paused = False
|
subscription.is_daily_paused = False
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(subscription)
|
await db.refresh(subscription)
|
||||||
@@ -2088,7 +2088,7 @@ async def purchase_tariff(
|
|||||||
try:
|
try:
|
||||||
# Determine if this is a new subscription or extension
|
# Determine if this is a new subscription or extension
|
||||||
was_new_subscription = (
|
was_new_subscription = (
|
||||||
subscription.start_date and (datetime.utcnow() - subscription.start_date).total_seconds() < 60
|
subscription.start_date and (datetime.now(UTC) - subscription.start_date).total_seconds() < 60
|
||||||
)
|
)
|
||||||
notification_type = (
|
notification_type = (
|
||||||
NotificationType.SUBSCRIPTION_ACTIVATED
|
NotificationType.SUBSCRIPTION_ACTIVATED
|
||||||
@@ -2124,7 +2124,7 @@ async def purchase_tariff(
|
|||||||
notification_service = AdminNotificationService(bot)
|
notification_service = AdminNotificationService(bot)
|
||||||
# Определяем тип покупки: новая подписка или продление
|
# Определяем тип покупки: новая подписка или продление
|
||||||
was_new_subscription = (
|
was_new_subscription = (
|
||||||
subscription.start_date and (datetime.utcnow() - subscription.start_date).total_seconds() < 60
|
subscription.start_date and (datetime.now(UTC) - subscription.start_date).total_seconds() < 60
|
||||||
)
|
)
|
||||||
await notification_service.send_subscription_purchase_notification(
|
await notification_service.send_subscription_purchase_notification(
|
||||||
db=db,
|
db=db,
|
||||||
@@ -2211,7 +2211,7 @@ async def purchase_devices(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Calculate prorated price based on remaining days
|
# Calculate prorated price based on remaining days
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
now = datetime.now(UTC)
|
now = datetime.now(UTC)
|
||||||
end_date = subscription.end_date
|
end_date = subscription.end_date
|
||||||
@@ -2627,7 +2627,7 @@ async def get_device_price(
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Calculate prorated price
|
# Calculate prorated price
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
now = datetime.now(UTC)
|
now = datetime.now(UTC)
|
||||||
end_date = subscription.end_date
|
end_date = subscription.end_date
|
||||||
@@ -3079,9 +3079,9 @@ async def get_available_countries(
|
|||||||
connected_squads = user.subscription.connected_squads or []
|
connected_squads = user.subscription.connected_squads or []
|
||||||
# Calculate days left for prorated pricing
|
# Calculate days left for prorated pricing
|
||||||
if user.subscription.end_date:
|
if user.subscription.end_date:
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
delta = user.subscription.end_date - datetime.utcnow()
|
delta = user.subscription.end_date - datetime.now(UTC)
|
||||||
days_left = max(0, delta.days)
|
days_left = max(0, delta.days)
|
||||||
|
|
||||||
# Get discount from promo group
|
# Get discount from promo group
|
||||||
@@ -3263,7 +3263,7 @@ async def update_countries(
|
|||||||
|
|
||||||
# Update connected squads
|
# Update connected squads
|
||||||
user.subscription.connected_squads = selected_countries
|
user.subscription.connected_squads = selected_countries
|
||||||
user.subscription.updated_at = datetime.utcnow()
|
user.subscription.updated_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
# Sync with RemnaWave
|
# Sync with RemnaWave
|
||||||
@@ -3910,7 +3910,7 @@ async def reduce_devices(
|
|||||||
|
|
||||||
# Update subscription
|
# Update subscription
|
||||||
subscription.device_limit = new_device_limit
|
subscription.device_limit = new_device_limit
|
||||||
subscription.updated_at = datetime.utcnow()
|
subscription.updated_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
# Update RemnaWave
|
# Update RemnaWave
|
||||||
@@ -4010,8 +4010,8 @@ async def preview_tariff_switch(
|
|||||||
|
|
||||||
# Calculate remaining days
|
# Calculate remaining days
|
||||||
remaining_days = 0
|
remaining_days = 0
|
||||||
if user.subscription.end_date and user.subscription.end_date > datetime.utcnow():
|
if user.subscription.end_date and user.subscription.end_date > datetime.now(UTC):
|
||||||
delta = user.subscription.end_date - datetime.utcnow()
|
delta = user.subscription.end_date - datetime.now(UTC)
|
||||||
remaining_days = max(0, delta.days)
|
remaining_days = max(0, delta.days)
|
||||||
|
|
||||||
# Calculate switch cost
|
# Calculate switch cost
|
||||||
@@ -4128,7 +4128,7 @@ async def switch_tariff(
|
|||||||
db: AsyncSession = Depends(get_cabinet_db),
|
db: AsyncSession = Depends(get_cabinet_db),
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Switch to a different tariff without changing end date."""
|
"""Switch to a different tariff without changing end date."""
|
||||||
from datetime import timedelta
|
from datetime import UTC, timedelta
|
||||||
|
|
||||||
if not settings.is_tariffs_mode():
|
if not settings.is_tariffs_mode():
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -4195,8 +4195,8 @@ async def switch_tariff(
|
|||||||
|
|
||||||
# Calculate remaining days
|
# Calculate remaining days
|
||||||
remaining_days = 0
|
remaining_days = 0
|
||||||
if user.subscription.end_date and user.subscription.end_date > datetime.utcnow():
|
if user.subscription.end_date and user.subscription.end_date > datetime.now(UTC):
|
||||||
delta = user.subscription.end_date - datetime.utcnow()
|
delta = user.subscription.end_date - datetime.now(UTC)
|
||||||
remaining_days = max(0, delta.days)
|
remaining_days = max(0, delta.days)
|
||||||
|
|
||||||
# Calculate cost
|
# Calculate cost
|
||||||
@@ -4334,14 +4334,14 @@ async def switch_tariff(
|
|||||||
|
|
||||||
if switching_to_daily:
|
if switching_to_daily:
|
||||||
# Switching TO daily - reset end_date to 1 day, set last_daily_charge_at
|
# Switching TO daily - reset end_date to 1 day, set last_daily_charge_at
|
||||||
user.subscription.end_date = datetime.utcnow() + timedelta(days=1)
|
user.subscription.end_date = datetime.now(UTC) + timedelta(days=1)
|
||||||
user.subscription.last_daily_charge_at = datetime.utcnow()
|
user.subscription.last_daily_charge_at = datetime.now(UTC)
|
||||||
user.subscription.is_daily_paused = False
|
user.subscription.is_daily_paused = False
|
||||||
elif switching_from_daily:
|
elif switching_from_daily:
|
||||||
user.subscription.end_date = datetime.utcnow() + timedelta(days=new_period_days)
|
user.subscription.end_date = datetime.now(UTC) + timedelta(days=new_period_days)
|
||||||
user.subscription.is_daily_paused = False
|
user.subscription.is_daily_paused = False
|
||||||
|
|
||||||
user.subscription.updated_at = datetime.utcnow()
|
user.subscription.updated_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
# Sync with RemnaWave
|
# Sync with RemnaWave
|
||||||
@@ -4425,7 +4425,7 @@ async def toggle_subscription_pause(
|
|||||||
db: AsyncSession = Depends(get_cabinet_db),
|
db: AsyncSession = Depends(get_cabinet_db),
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Toggle pause/resume for daily subscription."""
|
"""Toggle pause/resume for daily subscription."""
|
||||||
from datetime import timedelta
|
from datetime import UTC, timedelta
|
||||||
|
|
||||||
await db.refresh(user, ['subscription'])
|
await db.refresh(user, ['subscription'])
|
||||||
|
|
||||||
@@ -4476,8 +4476,8 @@ async def toggle_subscription_pause(
|
|||||||
# Restore ACTIVE status if was DISABLED
|
# Restore ACTIVE status if was DISABLED
|
||||||
if was_disabled:
|
if was_disabled:
|
||||||
user.subscription.status = SubscriptionStatus.ACTIVE.value
|
user.subscription.status = SubscriptionStatus.ACTIVE.value
|
||||||
user.subscription.last_daily_charge_at = datetime.utcnow()
|
user.subscription.last_daily_charge_at = datetime.now(UTC)
|
||||||
user.subscription.end_date = datetime.utcnow() + timedelta(days=1)
|
user.subscription.end_date = datetime.now(UTC) + timedelta(days=1)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(user.subscription)
|
await db.refresh(user.subscription)
|
||||||
@@ -4614,7 +4614,7 @@ async def switch_traffic_package(
|
|||||||
user.subscription.traffic_limit_gb = new_traffic
|
user.subscription.traffic_limit_gb = new_traffic
|
||||||
user.subscription.purchased_traffic_gb = 0 # Reset purchased traffic on switch
|
user.subscription.purchased_traffic_gb = 0 # Reset purchased traffic on switch
|
||||||
user.subscription.traffic_reset_at = None # Reset traffic reset date
|
user.subscription.traffic_reset_at = None # Reset traffic reset date
|
||||||
user.subscription.updated_at = datetime.utcnow()
|
user.subscription.updated_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
# Sync with RemnaWave
|
# Sync with RemnaWave
|
||||||
@@ -4733,7 +4733,7 @@ async def refresh_traffic(
|
|||||||
used_gb = traffic_stats.get('used_traffic_gb', 0)
|
used_gb = traffic_stats.get('used_traffic_gb', 0)
|
||||||
if abs((user.subscription.traffic_used_gb or 0) - used_gb) > 0.01:
|
if abs((user.subscription.traffic_used_gb or 0) - used_gb) > 0.01:
|
||||||
user.subscription.traffic_used_gb = used_gb
|
user.subscription.traffic_used_gb = used_gb
|
||||||
user.subscription.updated_at = datetime.utcnow()
|
user.subscription.updated_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
# Calculate percentage
|
# Calculate percentage
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Support tickets routes for cabinet."""
|
"""Support tickets routes for cabinet."""
|
||||||
|
|
||||||
import math
|
import math
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||||
@@ -137,8 +137,8 @@ async def create_ticket(
|
|||||||
title=request.title,
|
title=request.title,
|
||||||
status='open',
|
status='open',
|
||||||
priority='normal',
|
priority='normal',
|
||||||
created_at=datetime.utcnow(),
|
created_at=datetime.now(UTC),
|
||||||
updated_at=datetime.utcnow(),
|
updated_at=datetime.now(UTC),
|
||||||
)
|
)
|
||||||
db.add(ticket)
|
db.add(ticket)
|
||||||
await db.flush()
|
await db.flush()
|
||||||
@@ -152,7 +152,7 @@ async def create_ticket(
|
|||||||
media_type=request.media_type,
|
media_type=request.media_type,
|
||||||
media_file_id=request.media_file_id,
|
media_file_id=request.media_file_id,
|
||||||
media_caption=request.media_caption,
|
media_caption=request.media_caption,
|
||||||
created_at=datetime.utcnow(),
|
created_at=datetime.now(UTC),
|
||||||
)
|
)
|
||||||
db.add(message)
|
db.add(message)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
@@ -268,14 +268,14 @@ async def add_ticket_message(
|
|||||||
media_type=request.media_type,
|
media_type=request.media_type,
|
||||||
media_file_id=request.media_file_id,
|
media_file_id=request.media_file_id,
|
||||||
media_caption=request.media_caption,
|
media_caption=request.media_caption,
|
||||||
created_at=datetime.utcnow(),
|
created_at=datetime.now(UTC),
|
||||||
)
|
)
|
||||||
db.add(message)
|
db.add(message)
|
||||||
|
|
||||||
# Update ticket status and timestamp
|
# Update ticket status and timestamp
|
||||||
if ticket.status == 'answered':
|
if ticket.status == 'answered':
|
||||||
ticket.status = 'pending'
|
ticket.status = 'pending'
|
||||||
ticket.updated_at = datetime.utcnow()
|
ticket.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(message)
|
await db.refresh(message)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Service for managing email template overrides stored in the database.
|
|||||||
Custom templates override the hardcoded defaults from email_templates.py.
|
Custom templates override the hardcoded defaults from email_templates.py.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
@@ -124,7 +124,7 @@ async def save_template_override(
|
|||||||
)
|
)
|
||||||
row = existing.fetchone()
|
row = existing.fetchone()
|
||||||
|
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
|
|
||||||
if row:
|
if row:
|
||||||
# Update
|
# Update
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from sqlalchemy import and_, delete, func, select, update
|
from sqlalchemy import and_, delete, func, select, update
|
||||||
@@ -151,7 +151,7 @@ async def update_campaign(
|
|||||||
if not update_data:
|
if not update_data:
|
||||||
return campaign
|
return campaign
|
||||||
|
|
||||||
update_data['updated_at'] = datetime.utcnow()
|
update_data['updated_at'] = datetime.now(UTC)
|
||||||
|
|
||||||
await db.execute(update(AdvertisingCampaign).where(AdvertisingCampaign.id == campaign.id).values(**update_data))
|
await db.execute(update(AdvertisingCampaign).where(AdvertisingCampaign.id == campaign.id).values(**update_data))
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
@@ -127,7 +127,7 @@ async def update_cloudpayments_payment(
|
|||||||
if hasattr(payment, key):
|
if hasattr(payment, key):
|
||||||
setattr(payment, key, value)
|
setattr(payment, key, value)
|
||||||
|
|
||||||
payment.updated_at = datetime.utcnow()
|
payment.updated_at = datetime.now(UTC)
|
||||||
await db.flush()
|
await db.flush()
|
||||||
await db.refresh(payment)
|
await db.refresh(payment)
|
||||||
|
|
||||||
@@ -171,7 +171,7 @@ async def mark_cloudpayments_payment_as_paid(
|
|||||||
|
|
||||||
payment.status = 'completed'
|
payment.status = 'completed'
|
||||||
payment.is_paid = True
|
payment.is_paid = True
|
||||||
payment.paid_at = datetime.utcnow()
|
payment.paid_at = datetime.now(UTC)
|
||||||
|
|
||||||
if transaction_id_cp is not None:
|
if transaction_id_cp is not None:
|
||||||
payment.transaction_id_cp = transaction_id_cp
|
payment.transaction_id_cp = transaction_id_cp
|
||||||
@@ -190,7 +190,7 @@ async def mark_cloudpayments_payment_as_paid(
|
|||||||
if callback_payload:
|
if callback_payload:
|
||||||
payment.callback_payload = callback_payload
|
payment.callback_payload = callback_payload
|
||||||
|
|
||||||
payment.updated_at = datetime.utcnow()
|
payment.updated_at = datetime.now(UTC)
|
||||||
await db.flush()
|
await db.flush()
|
||||||
await db.refresh(payment)
|
await db.refresh(payment)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from sqlalchemy import and_, delete, desc, select
|
from sqlalchemy import and_, delete, desc, select
|
||||||
@@ -107,7 +107,7 @@ async def create_round(
|
|||||||
|
|
||||||
|
|
||||||
async def get_active_rounds(db: AsyncSession) -> list[ContestRound]:
|
async def get_active_rounds(db: AsyncSession) -> list[ContestRound]:
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(ContestRound)
|
select(ContestRound)
|
||||||
.options(selectinload(ContestRound.template))
|
.options(selectinload(ContestRound.template))
|
||||||
@@ -124,7 +124,7 @@ async def get_active_rounds(db: AsyncSession) -> list[ContestRound]:
|
|||||||
|
|
||||||
|
|
||||||
async def get_active_round_by_template(db: AsyncSession, template_id: int) -> ContestRound | None:
|
async def get_active_round_by_template(db: AsyncSession, template_id: int) -> ContestRound | None:
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(ContestRound)
|
select(ContestRound)
|
||||||
.options(selectinload(ContestRound.template))
|
.options(selectinload(ContestRound.template))
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from sqlalchemy import and_, select
|
from sqlalchemy import and_, select
|
||||||
@@ -76,7 +76,7 @@ async def update_cryptobot_payment_status(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
payment.status = status
|
payment.status = status
|
||||||
payment.updated_at = datetime.utcnow()
|
payment.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
if status == 'paid' and paid_at:
|
if status == 'paid' and paid_at:
|
||||||
payment.paid_at = paid_at
|
payment.paid_at = paid_at
|
||||||
@@ -97,7 +97,7 @@ async def link_cryptobot_payment_to_transaction(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
payment.transaction_id = transaction_id
|
payment.transaction_id = transaction_id
|
||||||
payment.updated_at = datetime.utcnow()
|
payment.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(payment)
|
await db.refresh(payment)
|
||||||
@@ -120,9 +120,9 @@ async def get_user_cryptobot_payments(
|
|||||||
|
|
||||||
|
|
||||||
async def get_pending_cryptobot_payments(db: AsyncSession, older_than_hours: int = 24) -> list[CryptoBotPayment]:
|
async def get_pending_cryptobot_payments(db: AsyncSession, older_than_hours: int = 24) -> list[CryptoBotPayment]:
|
||||||
from datetime import timedelta
|
from datetime import UTC, timedelta
|
||||||
|
|
||||||
cutoff_time = datetime.utcnow() - timedelta(hours=older_than_hours)
|
cutoff_time = datetime.now(UTC) - timedelta(hours=older_than_hours)
|
||||||
|
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(CryptoBotPayment)
|
select(CryptoBotPayment)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from sqlalchemy import func, select
|
from sqlalchemy import func, select
|
||||||
@@ -28,7 +28,7 @@ async def upsert_discount_offer(
|
|||||||
) -> DiscountOffer:
|
) -> DiscountOffer:
|
||||||
"""Create or refresh a discount offer for a user."""
|
"""Create or refresh a discount offer for a user."""
|
||||||
|
|
||||||
expires_at = datetime.utcnow() + timedelta(hours=valid_hours)
|
expires_at = datetime.now(UTC) + timedelta(hours=valid_hours)
|
||||||
|
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(DiscountOffer)
|
select(DiscountOffer)
|
||||||
@@ -116,7 +116,7 @@ async def list_active_discount_offers_for_user(
|
|||||||
) -> list[DiscountOffer]:
|
) -> list[DiscountOffer]:
|
||||||
"""Return active (not yet claimed) offers for a user."""
|
"""Return active (not yet claimed) offers for a user."""
|
||||||
|
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
stmt = (
|
stmt = (
|
||||||
select(DiscountOffer)
|
select(DiscountOffer)
|
||||||
.options(
|
.options(
|
||||||
@@ -161,7 +161,7 @@ async def mark_offer_claimed(
|
|||||||
*,
|
*,
|
||||||
details: dict | None = None,
|
details: dict | None = None,
|
||||||
) -> DiscountOffer:
|
) -> DiscountOffer:
|
||||||
offer.claimed_at = datetime.utcnow()
|
offer.claimed_at = datetime.now(UTC)
|
||||||
offer.is_active = False
|
offer.is_active = False
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(offer)
|
await db.refresh(offer)
|
||||||
@@ -190,7 +190,7 @@ async def mark_offer_claimed(
|
|||||||
|
|
||||||
|
|
||||||
async def deactivate_expired_offers(db: AsyncSession) -> int:
|
async def deactivate_expired_offers(db: AsyncSession) -> int:
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(DiscountOffer).where(
|
select(DiscountOffer).where(
|
||||||
DiscountOffer.is_active == True,
|
DiscountOffer.is_active == True,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from sqlalchemy import delete, func, select, update
|
from sqlalchemy import delete, func, select, update
|
||||||
@@ -21,7 +21,7 @@ async def set_faq_enabled(db: AsyncSession, language: str, enabled: bool) -> Faq
|
|||||||
|
|
||||||
if setting:
|
if setting:
|
||||||
setting.is_enabled = bool(enabled)
|
setting.is_enabled = bool(enabled)
|
||||||
setting.updated_at = datetime.utcnow()
|
setting.updated_at = datetime.now(UTC)
|
||||||
else:
|
else:
|
||||||
setting = FaqSetting(
|
setting = FaqSetting(
|
||||||
language=language,
|
language=language,
|
||||||
@@ -117,7 +117,7 @@ async def update_faq_page(
|
|||||||
if is_active is not None:
|
if is_active is not None:
|
||||||
page.is_active = bool(is_active)
|
page.is_active = bool(is_active)
|
||||||
|
|
||||||
page.updated_at = datetime.utcnow()
|
page.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(page)
|
await db.refresh(page)
|
||||||
@@ -139,6 +139,6 @@ async def bulk_update_order(
|
|||||||
) -> None:
|
) -> None:
|
||||||
for page_id, order in pages:
|
for page_id, order in pages:
|
||||||
await db.execute(
|
await db.execute(
|
||||||
update(FaqPage).where(FaqPage.id == page_id).values(display_order=order, updated_at=datetime.utcnow())
|
update(FaqPage).where(FaqPage.id == page_id).values(display_order=order, updated_at=datetime.now(UTC))
|
||||||
)
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""CRUD операции для платежей Freekassa."""
|
"""CRUD операции для платежей Freekassa."""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
@@ -77,10 +77,10 @@ async def update_freekassa_payment_status(
|
|||||||
"""Обновляет статус платежа."""
|
"""Обновляет статус платежа."""
|
||||||
payment.status = status
|
payment.status = status
|
||||||
payment.is_paid = is_paid
|
payment.is_paid = is_paid
|
||||||
payment.updated_at = datetime.utcnow()
|
payment.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
if is_paid:
|
if is_paid:
|
||||||
payment.paid_at = datetime.utcnow()
|
payment.paid_at = datetime.now(UTC)
|
||||||
if freekassa_order_id:
|
if freekassa_order_id:
|
||||||
payment.freekassa_order_id = freekassa_order_id
|
payment.freekassa_order_id = freekassa_order_id
|
||||||
if payment_system_id is not None:
|
if payment_system_id is not None:
|
||||||
@@ -134,7 +134,7 @@ async def get_expired_pending_payments(
|
|||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
) -> list[FreekassaPayment]:
|
) -> list[FreekassaPayment]:
|
||||||
"""Получает просроченные платежи в статусе pending."""
|
"""Получает просроченные платежи в статусе pending."""
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(FreekassaPayment).where(
|
select(FreekassaPayment).where(
|
||||||
FreekassaPayment.status == 'pending',
|
FreekassaPayment.status == 'pending',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
@@ -129,7 +129,7 @@ async def update_heleket_payment(
|
|||||||
if paid_at is not None:
|
if paid_at is not None:
|
||||||
payment.paid_at = paid_at
|
payment.paid_at = paid_at
|
||||||
|
|
||||||
payment.updated_at = datetime.utcnow()
|
payment.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(payment)
|
await db.refresh(payment)
|
||||||
@@ -157,7 +157,7 @@ async def link_heleket_payment_to_transaction(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
payment.transaction_id = transaction_id
|
payment.transaction_id = transaction_id
|
||||||
payment.updated_at = datetime.utcnow()
|
payment.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(payment)
|
await db.refresh(payment)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""CRUD операции для платежей KassaAI."""
|
"""CRUD операции для платежей KassaAI."""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
@@ -79,10 +79,10 @@ async def update_kassa_ai_payment_status(
|
|||||||
"""Обновляет статус платежа."""
|
"""Обновляет статус платежа."""
|
||||||
payment.status = status
|
payment.status = status
|
||||||
payment.is_paid = is_paid
|
payment.is_paid = is_paid
|
||||||
payment.updated_at = datetime.utcnow()
|
payment.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
if is_paid:
|
if is_paid:
|
||||||
payment.paid_at = datetime.utcnow()
|
payment.paid_at = datetime.now(UTC)
|
||||||
if kassa_ai_order_id:
|
if kassa_ai_order_id:
|
||||||
payment.kassa_ai_order_id = kassa_ai_order_id
|
payment.kassa_ai_order_id = kassa_ai_order_id
|
||||||
if payment_system_id is not None:
|
if payment_system_id is not None:
|
||||||
@@ -136,7 +136,7 @@ async def get_expired_pending_kassa_ai_payments(
|
|||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
) -> list[KassaAiPayment]:
|
) -> list[KassaAiPayment]:
|
||||||
"""Получает просроченные платежи в статусе pending."""
|
"""Получает просроченные платежи в статусе pending."""
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(KassaAiPayment).where(
|
select(KassaAiPayment).where(
|
||||||
KassaAiPayment.status == 'pending',
|
KassaAiPayment.status == 'pending',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
@@ -90,7 +90,7 @@ async def update_mulenpay_payment_status(
|
|||||||
if metadata is not None:
|
if metadata is not None:
|
||||||
payment.metadata_json = metadata
|
payment.metadata_json = metadata
|
||||||
|
|
||||||
payment.updated_at = datetime.utcnow()
|
payment.updated_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(payment)
|
await db.refresh(payment)
|
||||||
return payment
|
return payment
|
||||||
@@ -103,7 +103,7 @@ async def update_mulenpay_payment_metadata(
|
|||||||
metadata: dict,
|
metadata: dict,
|
||||||
) -> MulenPayPayment:
|
) -> MulenPayPayment:
|
||||||
payment.metadata_json = metadata
|
payment.metadata_json = metadata
|
||||||
payment.updated_at = datetime.utcnow()
|
payment.updated_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(payment)
|
await db.refresh(payment)
|
||||||
return payment
|
return payment
|
||||||
@@ -116,7 +116,7 @@ async def link_mulenpay_payment_to_transaction(
|
|||||||
transaction_id: int,
|
transaction_id: int,
|
||||||
) -> MulenPayPayment:
|
) -> MulenPayPayment:
|
||||||
payment.transaction_id = transaction_id
|
payment.transaction_id = transaction_id
|
||||||
payment.updated_at = datetime.utcnow()
|
payment.updated_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(payment)
|
await db.refresh(payment)
|
||||||
return payment
|
return payment
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
@@ -115,7 +115,7 @@ async def update_platega_payment(
|
|||||||
if expires_at is not None:
|
if expires_at is not None:
|
||||||
payment.expires_at = expires_at
|
payment.expires_at = expires_at
|
||||||
|
|
||||||
payment.updated_at = datetime.utcnow()
|
payment.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(payment)
|
await db.refresh(payment)
|
||||||
@@ -129,7 +129,7 @@ async def link_platega_payment_to_transaction(
|
|||||||
transaction_id: int,
|
transaction_id: int,
|
||||||
) -> PlategaPayment:
|
) -> PlategaPayment:
|
||||||
payment.transaction_id = transaction_id
|
payment.transaction_id = transaction_id
|
||||||
payment.updated_at = datetime.utcnow()
|
payment.updated_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(payment)
|
await db.refresh(payment)
|
||||||
return payment
|
return payment
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
@@ -26,7 +26,7 @@ async def upsert_privacy_policy(
|
|||||||
|
|
||||||
if policy:
|
if policy:
|
||||||
policy.content = content or ''
|
policy.content = content or ''
|
||||||
policy.updated_at = datetime.utcnow()
|
policy.updated_at = datetime.now(UTC)
|
||||||
else:
|
else:
|
||||||
policy = PrivacyPolicy(
|
policy = PrivacyPolicy(
|
||||||
language=language,
|
language=language,
|
||||||
@@ -52,7 +52,7 @@ async def set_privacy_policy_enabled(
|
|||||||
|
|
||||||
if policy:
|
if policy:
|
||||||
policy.is_enabled = bool(enabled)
|
policy.is_enabled = bool(enabled)
|
||||||
policy.updated_at = datetime.utcnow()
|
policy.updated_at = datetime.now(UTC)
|
||||||
else:
|
else:
|
||||||
policy = PrivacyPolicy(
|
policy = PrivacyPolicy(
|
||||||
language=language,
|
language=language,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
@@ -118,13 +118,13 @@ async def ensure_default_templates(db: AsyncSession, *, created_by: int | None =
|
|||||||
|
|
||||||
if should_update and new_message:
|
if should_update and new_message:
|
||||||
existing.message_text = new_message
|
existing.message_text = new_message
|
||||||
existing.updated_at = datetime.utcnow()
|
existing.updated_at = datetime.now(UTC)
|
||||||
await db.flush()
|
await db.flush()
|
||||||
|
|
||||||
target_active_hours = template_data.get('active_discount_hours')
|
target_active_hours = template_data.get('active_discount_hours')
|
||||||
if target_active_hours is not None and target_active_hours > 0 and not existing.active_discount_hours:
|
if target_active_hours is not None and target_active_hours > 0 and not existing.active_discount_hours:
|
||||||
existing.active_discount_hours = target_active_hours
|
existing.active_discount_hours = target_active_hours
|
||||||
existing.updated_at = datetime.utcnow()
|
existing.updated_at = datetime.now(UTC)
|
||||||
await db.flush()
|
await db.flush()
|
||||||
templates.append(existing)
|
templates.append(existing)
|
||||||
continue
|
continue
|
||||||
@@ -204,7 +204,7 @@ async def update_promo_offer_template(
|
|||||||
if is_active is not None:
|
if is_active is not None:
|
||||||
template.is_active = is_active
|
template.is_active = is_active
|
||||||
|
|
||||||
template.updated_at = datetime.utcnow()
|
template.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(template)
|
await db.refresh(template)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from sqlalchemy import and_, func, select
|
from sqlalchemy import and_, func, select
|
||||||
@@ -114,7 +114,7 @@ async def check_user_promocode_usage(db: AsyncSession, user_id: int, promocode_i
|
|||||||
|
|
||||||
|
|
||||||
async def create_promocode_use(db: AsyncSession, promocode_id: int, user_id: int) -> PromoCodeUse:
|
async def create_promocode_use(db: AsyncSession, promocode_id: int, user_id: int) -> PromoCodeUse:
|
||||||
promocode_use = PromoCodeUse(promocode_id=promocode_id, user_id=user_id, used_at=datetime.utcnow())
|
promocode_use = PromoCodeUse(promocode_id=promocode_id, user_id=user_id, used_at=datetime.now(UTC))
|
||||||
|
|
||||||
db.add(promocode_use)
|
db.add(promocode_use)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
@@ -133,9 +133,9 @@ async def get_promocode_use_by_user_and_code(db: AsyncSession, user_id: int, pro
|
|||||||
|
|
||||||
async def count_user_recent_activations(db: AsyncSession, user_id: int, hours: int = 24) -> int:
|
async def count_user_recent_activations(db: AsyncSession, user_id: int, hours: int = 24) -> int:
|
||||||
"""Подсчитывает количество активаций промокодов пользователем за последние N часов."""
|
"""Подсчитывает количество активаций промокодов пользователем за последние N часов."""
|
||||||
from datetime import timedelta
|
from datetime import UTC, timedelta
|
||||||
|
|
||||||
cutoff = datetime.utcnow() - timedelta(hours=hours)
|
cutoff = datetime.now(UTC) - timedelta(hours=hours)
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(func.count(PromoCodeUse.id)).where(and_(PromoCodeUse.user_id == user_id, PromoCodeUse.used_at >= cutoff))
|
select(func.count(PromoCodeUse.id)).where(and_(PromoCodeUse.user_id == user_id, PromoCodeUse.used_at >= cutoff))
|
||||||
)
|
)
|
||||||
@@ -178,7 +178,7 @@ async def update_promocode(db: AsyncSession, promocode: PromoCode, **kwargs) ->
|
|||||||
if hasattr(promocode, field):
|
if hasattr(promocode, field):
|
||||||
setattr(promocode, field, value)
|
setattr(promocode, field, value)
|
||||||
|
|
||||||
promocode.updated_at = datetime.utcnow()
|
promocode.updated_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(promocode)
|
await db.refresh(promocode)
|
||||||
|
|
||||||
@@ -239,7 +239,7 @@ async def get_promocode_statistics(db: AsyncSession, promocode_id: int) -> dict:
|
|||||||
)
|
)
|
||||||
total_uses = total_uses_result.scalar()
|
total_uses = total_uses_result.scalar()
|
||||||
|
|
||||||
today = datetime.utcnow().date()
|
today = datetime.now(UTC).date()
|
||||||
today_uses_result = await db.execute(
|
today_uses_result = await db.execute(
|
||||||
select(func.count(PromoCodeUse.id)).where(
|
select(func.count(PromoCodeUse.id)).where(
|
||||||
and_(PromoCodeUse.promocode_id == promocode_id, PromoCodeUse.used_at >= today)
|
and_(PromoCodeUse.promocode_id == promocode_id, PromoCodeUse.used_at >= today)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
@@ -26,7 +26,7 @@ async def upsert_public_offer(
|
|||||||
|
|
||||||
if offer:
|
if offer:
|
||||||
offer.content = content or ''
|
offer.content = content or ''
|
||||||
offer.updated_at = datetime.utcnow()
|
offer.updated_at = datetime.now(UTC)
|
||||||
else:
|
else:
|
||||||
offer = PublicOffer(
|
offer = PublicOffer(
|
||||||
language=language,
|
language=language,
|
||||||
@@ -52,7 +52,7 @@ async def set_public_offer_enabled(
|
|||||||
|
|
||||||
if offer:
|
if offer:
|
||||||
offer.is_enabled = bool(enabled)
|
offer.is_enabled = bool(enabled)
|
||||||
offer.updated_at = datetime.utcnow()
|
offer.updated_at = datetime.now(UTC)
|
||||||
else:
|
else:
|
||||||
offer = PublicOffer(
|
offer = PublicOffer(
|
||||||
language=language,
|
language=language,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from sqlalchemy import and_, func, select
|
from sqlalchemy import and_, func, select
|
||||||
@@ -178,7 +178,7 @@ async def get_referral_statistics(db: AsyncSession) -> dict:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
today = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
|
today = datetime.now(UTC).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
|
||||||
today_referral_earnings_result = await db.execute(
|
today_referral_earnings_result = await db.execute(
|
||||||
select(func.coalesce(func.sum(ReferralEarning.amount_kopeks), 0)).where(ReferralEarning.created_at >= today)
|
select(func.coalesce(func.sum(ReferralEarning.amount_kopeks), 0)).where(ReferralEarning.created_at >= today)
|
||||||
@@ -190,7 +190,7 @@ async def get_referral_statistics(db: AsyncSession) -> dict:
|
|||||||
)
|
)
|
||||||
today_earnings = today_referral_earnings_result.scalar() + today_transaction_earnings_result.scalar()
|
today_earnings = today_referral_earnings_result.scalar() + today_transaction_earnings_result.scalar()
|
||||||
|
|
||||||
week_ago = datetime.utcnow() - timedelta(days=7)
|
week_ago = datetime.now(UTC) - timedelta(days=7)
|
||||||
week_referral_earnings_result = await db.execute(
|
week_referral_earnings_result = await db.execute(
|
||||||
select(func.coalesce(func.sum(ReferralEarning.amount_kopeks), 0)).where(ReferralEarning.created_at >= week_ago)
|
select(func.coalesce(func.sum(ReferralEarning.amount_kopeks), 0)).where(ReferralEarning.created_at >= week_ago)
|
||||||
)
|
)
|
||||||
@@ -201,7 +201,7 @@ async def get_referral_statistics(db: AsyncSession) -> dict:
|
|||||||
)
|
)
|
||||||
week_earnings = week_referral_earnings_result.scalar() + week_transaction_earnings_result.scalar()
|
week_earnings = week_referral_earnings_result.scalar() + week_transaction_earnings_result.scalar()
|
||||||
|
|
||||||
month_ago = datetime.utcnow() - timedelta(days=30)
|
month_ago = datetime.now(UTC) - timedelta(days=30)
|
||||||
month_referral_earnings_result = await db.execute(
|
month_referral_earnings_result = await db.execute(
|
||||||
select(func.coalesce(func.sum(ReferralEarning.amount_kopeks), 0)).where(ReferralEarning.created_at >= month_ago)
|
select(func.coalesce(func.sum(ReferralEarning.amount_kopeks), 0)).where(ReferralEarning.created_at >= month_ago)
|
||||||
)
|
)
|
||||||
@@ -249,7 +249,7 @@ async def get_top_referrers_by_period(
|
|||||||
"""
|
"""
|
||||||
from app.database.models import Transaction, TransactionType
|
from app.database.models import Transaction, TransactionType
|
||||||
|
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
if period == 'week':
|
if period == 'week':
|
||||||
start_date = now - timedelta(days=7)
|
start_date = now - timedelta(days=7)
|
||||||
else: # month
|
else: # month
|
||||||
@@ -380,12 +380,12 @@ async def get_user_referral_stats(db: AsyncSession, user_id: int) -> dict:
|
|||||||
|
|
||||||
total_earned = await get_referral_earnings_sum(db, user_id)
|
total_earned = await get_referral_earnings_sum(db, user_id)
|
||||||
|
|
||||||
month_ago = datetime.utcnow() - timedelta(days=30)
|
month_ago = datetime.now(UTC) - timedelta(days=30)
|
||||||
month_earned = await get_referral_earnings_sum(db, user_id, start_date=month_ago)
|
month_earned = await get_referral_earnings_sum(db, user_id, start_date=month_ago)
|
||||||
|
|
||||||
from app.database.models import Subscription, SubscriptionStatus
|
from app.database.models import Subscription, SubscriptionStatus
|
||||||
|
|
||||||
current_time = datetime.utcnow()
|
current_time = datetime.now(UTC)
|
||||||
|
|
||||||
active_referrals_result = await db.execute(
|
active_referrals_result = await db.execute(
|
||||||
select(func.count(User.id))
|
select(func.count(User.id))
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from datetime import date, datetime, time
|
from datetime import UTC, date, datetime, time
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from sqlalchemy import and_, desc, func, select
|
from sqlalchemy import and_, desc, func, select
|
||||||
@@ -165,7 +165,7 @@ async def add_contest_event(
|
|||||||
referral_id=referral_id,
|
referral_id=referral_id,
|
||||||
amount_kopeks=amount_kopeks,
|
amount_kopeks=amount_kopeks,
|
||||||
event_type=event_type,
|
event_type=event_type,
|
||||||
occurred_at=datetime.utcnow(),
|
occurred_at=datetime.now(UTC),
|
||||||
)
|
)
|
||||||
db.add(event)
|
db.add(event)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
@@ -512,7 +512,7 @@ async def upsert_contest_event(
|
|||||||
referral_id=referral_id,
|
referral_id=referral_id,
|
||||||
amount_kopeks=amount_kopeks,
|
amount_kopeks=amount_kopeks,
|
||||||
event_type=event_type,
|
event_type=event_type,
|
||||||
occurred_at=datetime.utcnow(),
|
occurred_at=datetime.now(UTC),
|
||||||
)
|
)
|
||||||
db.add(event)
|
db.add(event)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from sqlalchemy import select, update
|
from sqlalchemy import select, update
|
||||||
@@ -30,7 +30,7 @@ async def create_or_update_rules(
|
|||||||
|
|
||||||
for rule in existing_rules:
|
for rule in existing_rules:
|
||||||
rule.is_active = False
|
rule.is_active = False
|
||||||
rule.updated_at = datetime.utcnow()
|
rule.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
new_rules = ServiceRule(title=title, content=content, language=language, is_active=True, order=0)
|
new_rules = ServiceRule(title=title, content=content, language=language, is_active=True, order=0)
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ async def clear_all_rules(db: AsyncSession, language: str = 'ru') -> bool:
|
|||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
update(ServiceRule)
|
update(ServiceRule)
|
||||||
.where(ServiceRule.language == language, ServiceRule.is_active == True)
|
.where(ServiceRule.language == language, ServiceRule.is_active == True)
|
||||||
.values(is_active=False, updated_at=datetime.utcnow())
|
.values(is_active=False, updated_at=datetime.now(UTC))
|
||||||
)
|
)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
@@ -110,7 +110,7 @@ async def restore_rules_version(db: AsyncSession, rule_id: int, language: str =
|
|||||||
await db.execute(
|
await db.execute(
|
||||||
update(ServiceRule)
|
update(ServiceRule)
|
||||||
.where(ServiceRule.language == language, ServiceRule.is_active == True)
|
.where(ServiceRule.language == language, ServiceRule.is_active == True)
|
||||||
.values(is_active=False, updated_at=datetime.utcnow())
|
.values(is_active=False, updated_at=datetime.now(UTC))
|
||||||
)
|
)
|
||||||
|
|
||||||
restored_rule = ServiceRule(
|
restored_rule = ServiceRule(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import random
|
import random
|
||||||
from collections.abc import Iterable, Sequence
|
from collections.abc import Iterable, Sequence
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
@@ -360,7 +360,7 @@ async def sync_with_remnawave(db: AsyncSession, remnawave_squads: list[dict]) ->
|
|||||||
|
|
||||||
if len(filtered_squads) != len(current_squads):
|
if len(filtered_squads) != len(current_squads):
|
||||||
subscription.connected_squads = filtered_squads
|
subscription.connected_squads = filtered_squads
|
||||||
subscription.updated_at = datetime.utcnow()
|
subscription.updated_at = datetime.now(UTC)
|
||||||
cleaned_subscriptions += 1
|
cleaned_subscriptions += 1
|
||||||
|
|
||||||
# Clean up stale UUIDs from tariff allowed_squads
|
# Clean up stale UUIDs from tariff allowed_squads
|
||||||
@@ -373,7 +373,7 @@ async def sync_with_remnawave(db: AsyncSession, remnawave_squads: list[dict]) ->
|
|||||||
filtered = [u for u in current if u not in removed_uuids]
|
filtered = [u for u in current if u not in removed_uuids]
|
||||||
if len(filtered) != len(current):
|
if len(filtered) != len(current):
|
||||||
tariff.allowed_squads = filtered
|
tariff.allowed_squads = filtered
|
||||||
tariff.updated_at = datetime.utcnow()
|
tariff.updated_at = datetime.now(UTC)
|
||||||
cleaned_tariffs += 1
|
cleaned_tariffs += 1
|
||||||
logger.info(
|
logger.info(
|
||||||
'🧹 Тариф "%s" (ID: %s): удалены несуществующие сквады %s',
|
'🧹 Тариф "%s" (ID: %s): удалены несуществующие сквады %s',
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ def is_recently_updated_by_webhook(subscription: Subscription) -> bool:
|
|||||||
"""Return True if subscription was updated by webhook within guard window."""
|
"""Return True if subscription was updated by webhook within guard window."""
|
||||||
if not subscription.last_webhook_update_at:
|
if not subscription.last_webhook_update_at:
|
||||||
return False
|
return False
|
||||||
elapsed = (datetime.now(UTC).replace(tzinfo=None) - subscription.last_webhook_update_at).total_seconds()
|
elapsed = (datetime.now(UTC) - subscription.last_webhook_update_at).total_seconds()
|
||||||
return elapsed < _WEBHOOK_GUARD_SECONDS
|
return elapsed < _WEBHOOK_GUARD_SECONDS
|
||||||
|
|
||||||
|
|
||||||
@@ -101,13 +101,13 @@ async def create_trial_subscription(
|
|||||||
except Exception as error:
|
except Exception as error:
|
||||||
logger.error('Не удалось получить сквад для триальной подписки пользователя', user_id=user_id, error=error)
|
logger.error('Не удалось получить сквад для триальной подписки пользователя', user_id=user_id, error=error)
|
||||||
|
|
||||||
end_date = datetime.utcnow() + timedelta(days=duration_days)
|
end_date = datetime.now(UTC) + timedelta(days=duration_days)
|
||||||
|
|
||||||
# Check for existing PENDING trial subscription (retry after failed payment)
|
# Check for existing PENDING trial subscription (retry after failed payment)
|
||||||
existing = await get_subscription_by_user_id(db, user_id)
|
existing = await get_subscription_by_user_id(db, user_id)
|
||||||
if existing and existing.is_trial and existing.status == SubscriptionStatus.PENDING.value:
|
if existing and existing.is_trial and existing.status == SubscriptionStatus.PENDING.value:
|
||||||
existing.status = SubscriptionStatus.ACTIVE.value
|
existing.status = SubscriptionStatus.ACTIVE.value
|
||||||
existing.start_date = datetime.utcnow()
|
existing.start_date = datetime.now(UTC)
|
||||||
existing.end_date = end_date
|
existing.end_date = end_date
|
||||||
existing.traffic_limit_gb = traffic_limit_gb
|
existing.traffic_limit_gb = traffic_limit_gb
|
||||||
existing.device_limit = device_limit
|
existing.device_limit = device_limit
|
||||||
@@ -124,7 +124,7 @@ async def create_trial_subscription(
|
|||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
status=SubscriptionStatus.ACTIVE.value,
|
status=SubscriptionStatus.ACTIVE.value,
|
||||||
is_trial=True,
|
is_trial=True,
|
||||||
start_date=datetime.utcnow(),
|
start_date=datetime.now(UTC),
|
||||||
end_date=end_date,
|
end_date=end_date,
|
||||||
traffic_limit_gb=traffic_limit_gb,
|
traffic_limit_gb=traffic_limit_gb,
|
||||||
device_limit=device_limit,
|
device_limit=device_limit,
|
||||||
@@ -176,7 +176,7 @@ async def create_paid_subscription(
|
|||||||
is_trial: bool = False,
|
is_trial: bool = False,
|
||||||
tariff_id: int | None = None,
|
tariff_id: int | None = None,
|
||||||
) -> Subscription:
|
) -> Subscription:
|
||||||
end_date = datetime.utcnow() + timedelta(days=duration_days)
|
end_date = datetime.now(UTC) + timedelta(days=duration_days)
|
||||||
|
|
||||||
if device_limit is None:
|
if device_limit is None:
|
||||||
device_limit = settings.DEFAULT_DEVICE_LIMIT
|
device_limit = settings.DEFAULT_DEVICE_LIMIT
|
||||||
@@ -185,7 +185,7 @@ async def create_paid_subscription(
|
|||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
status=SubscriptionStatus.ACTIVE.value,
|
status=SubscriptionStatus.ACTIVE.value,
|
||||||
is_trial=is_trial,
|
is_trial=is_trial,
|
||||||
start_date=datetime.utcnow(),
|
start_date=datetime.now(UTC),
|
||||||
end_date=end_date,
|
end_date=end_date,
|
||||||
traffic_limit_gb=traffic_limit_gb,
|
traffic_limit_gb=traffic_limit_gb,
|
||||||
device_limit=device_limit,
|
device_limit=device_limit,
|
||||||
@@ -253,7 +253,7 @@ async def replace_subscription(
|
|||||||
) -> Subscription:
|
) -> Subscription:
|
||||||
"""Перезаписывает параметры существующей подписки пользователя."""
|
"""Перезаписывает параметры существующей подписки пользователя."""
|
||||||
|
|
||||||
current_time = datetime.utcnow()
|
current_time = datetime.now(UTC)
|
||||||
old_squads = set(subscription.connected_squads or [])
|
old_squads = set(subscription.connected_squads or [])
|
||||||
new_squads = set(connected_squads or [])
|
new_squads = set(connected_squads or [])
|
||||||
|
|
||||||
@@ -341,7 +341,7 @@ async def extend_subscription(
|
|||||||
device_limit: Лимит устройств (опционально, для режима тарифов)
|
device_limit: Лимит устройств (опционально, для режима тарифов)
|
||||||
connected_squads: Список UUID сквадов (опционально, для режима тарифов)
|
connected_squads: Список UUID сквадов (опционально, для режима тарифов)
|
||||||
"""
|
"""
|
||||||
current_time = datetime.utcnow()
|
current_time = datetime.now(UTC)
|
||||||
|
|
||||||
logger.info('🔄 Продление подписки на дней', subscription_id=subscription.id, days=days)
|
logger.info('🔄 Продление подписки на дней', subscription_id=subscription.id, days=days)
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -535,16 +535,16 @@ async def extend_subscription(
|
|||||||
|
|
||||||
async def add_subscription_traffic(db: AsyncSession, subscription: Subscription, gb: int) -> Subscription:
|
async def add_subscription_traffic(db: AsyncSession, subscription: Subscription, gb: int) -> Subscription:
|
||||||
subscription.add_traffic(gb)
|
subscription.add_traffic(gb)
|
||||||
subscription.updated_at = datetime.utcnow()
|
subscription.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
# Создаём новую запись докупки с индивидуальной датой истечения (30 дней)
|
# Создаём новую запись докупки с индивидуальной датой истечения (30 дней)
|
||||||
from datetime import timedelta
|
from datetime import UTC, timedelta
|
||||||
|
|
||||||
from sqlalchemy import select as sql_select
|
from sqlalchemy import select as sql_select
|
||||||
|
|
||||||
from app.database.models import TrafficPurchase
|
from app.database.models import TrafficPurchase
|
||||||
|
|
||||||
new_expires_at = datetime.utcnow() + timedelta(days=30)
|
new_expires_at = datetime.now(UTC) + timedelta(days=30)
|
||||||
new_purchase = TrafficPurchase(subscription_id=subscription.id, traffic_gb=gb, expires_at=new_expires_at)
|
new_purchase = TrafficPurchase(subscription_id=subscription.id, traffic_gb=gb, expires_at=new_expires_at)
|
||||||
db.add(new_purchase)
|
db.add(new_purchase)
|
||||||
|
|
||||||
@@ -553,7 +553,7 @@ async def add_subscription_traffic(db: AsyncSession, subscription: Subscription,
|
|||||||
subscription.purchased_traffic_gb = current_purchased + gb
|
subscription.purchased_traffic_gb = current_purchased + gb
|
||||||
|
|
||||||
# Устанавливаем traffic_reset_at на ближайшую дату истечения из всех активных докупок
|
# Устанавливаем traffic_reset_at на ближайшую дату истечения из всех активных докупок
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
active_purchases_query = (
|
active_purchases_query = (
|
||||||
sql_select(TrafficPurchase)
|
sql_select(TrafficPurchase)
|
||||||
.where(TrafficPurchase.subscription_id == subscription.id)
|
.where(TrafficPurchase.subscription_id == subscription.id)
|
||||||
@@ -585,7 +585,7 @@ async def add_subscription_traffic(db: AsyncSession, subscription: Subscription,
|
|||||||
|
|
||||||
async def add_subscription_devices(db: AsyncSession, subscription: Subscription, devices: int) -> Subscription:
|
async def add_subscription_devices(db: AsyncSession, subscription: Subscription, devices: int) -> Subscription:
|
||||||
subscription.device_limit += devices
|
subscription.device_limit += devices
|
||||||
subscription.updated_at = datetime.utcnow()
|
subscription.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(subscription)
|
await db.refresh(subscription)
|
||||||
@@ -597,7 +597,7 @@ async def add_subscription_devices(db: AsyncSession, subscription: Subscription,
|
|||||||
async def add_subscription_squad(db: AsyncSession, subscription: Subscription, squad_uuid: str) -> Subscription:
|
async def add_subscription_squad(db: AsyncSession, subscription: Subscription, squad_uuid: str) -> Subscription:
|
||||||
if squad_uuid not in subscription.connected_squads:
|
if squad_uuid not in subscription.connected_squads:
|
||||||
subscription.connected_squads = subscription.connected_squads + [squad_uuid]
|
subscription.connected_squads = subscription.connected_squads + [squad_uuid]
|
||||||
subscription.updated_at = datetime.utcnow()
|
subscription.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(subscription)
|
await db.refresh(subscription)
|
||||||
@@ -612,7 +612,7 @@ async def remove_subscription_squad(db: AsyncSession, subscription: Subscription
|
|||||||
squads = subscription.connected_squads.copy()
|
squads = subscription.connected_squads.copy()
|
||||||
squads.remove(squad_uuid)
|
squads.remove(squad_uuid)
|
||||||
subscription.connected_squads = squads
|
subscription.connected_squads = squads
|
||||||
subscription.updated_at = datetime.utcnow()
|
subscription.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(subscription)
|
await db.refresh(subscription)
|
||||||
@@ -688,7 +688,7 @@ async def update_subscription_autopay(
|
|||||||
) -> Subscription:
|
) -> Subscription:
|
||||||
subscription.autopay_enabled = enabled
|
subscription.autopay_enabled = enabled
|
||||||
subscription.autopay_days_before = days_before
|
subscription.autopay_days_before = days_before
|
||||||
subscription.updated_at = datetime.utcnow()
|
subscription.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(subscription)
|
await db.refresh(subscription)
|
||||||
@@ -700,7 +700,7 @@ async def update_subscription_autopay(
|
|||||||
|
|
||||||
async def deactivate_subscription(db: AsyncSession, subscription: Subscription) -> Subscription:
|
async def deactivate_subscription(db: AsyncSession, subscription: Subscription) -> Subscription:
|
||||||
subscription.status = SubscriptionStatus.DISABLED.value
|
subscription.status = SubscriptionStatus.DISABLED.value
|
||||||
subscription.updated_at = datetime.utcnow()
|
subscription.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(subscription)
|
await db.refresh(subscription)
|
||||||
@@ -715,7 +715,7 @@ async def reactivate_subscription(db: AsyncSession, subscription: Subscription)
|
|||||||
Активирует только если подписка была DISABLED и ещё не истекла.
|
Активирует только если подписка была DISABLED и ещё не истекла.
|
||||||
Не логирует если реактивация не требуется.
|
Не логирует если реактивация не требуется.
|
||||||
"""
|
"""
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
|
|
||||||
# Тихо выходим если реактивация не нужна
|
# Тихо выходим если реактивация не нужна
|
||||||
if subscription.status != SubscriptionStatus.DISABLED.value:
|
if subscription.status != SubscriptionStatus.DISABLED.value:
|
||||||
@@ -734,7 +734,7 @@ async def reactivate_subscription(db: AsyncSession, subscription: Subscription)
|
|||||||
|
|
||||||
|
|
||||||
async def get_expiring_subscriptions(db: AsyncSession, days_before: int = 3) -> list[Subscription]:
|
async def get_expiring_subscriptions(db: AsyncSession, days_before: int = 3) -> list[Subscription]:
|
||||||
threshold_date = datetime.utcnow() + timedelta(days=days_before)
|
threshold_date = datetime.now(UTC) + timedelta(days=days_before)
|
||||||
|
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(Subscription)
|
select(Subscription)
|
||||||
@@ -743,7 +743,7 @@ async def get_expiring_subscriptions(db: AsyncSession, days_before: int = 3) ->
|
|||||||
and_(
|
and_(
|
||||||
Subscription.status == SubscriptionStatus.ACTIVE.value,
|
Subscription.status == SubscriptionStatus.ACTIVE.value,
|
||||||
Subscription.end_date <= threshold_date,
|
Subscription.end_date <= threshold_date,
|
||||||
Subscription.end_date > datetime.utcnow(),
|
Subscription.end_date > datetime.now(UTC),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -754,13 +754,13 @@ async def get_expired_subscriptions(db: AsyncSession) -> list[Subscription]:
|
|||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(Subscription)
|
select(Subscription)
|
||||||
.options(selectinload(Subscription.user))
|
.options(selectinload(Subscription.user))
|
||||||
.where(and_(Subscription.status == SubscriptionStatus.ACTIVE.value, Subscription.end_date <= datetime.utcnow()))
|
.where(and_(Subscription.status == SubscriptionStatus.ACTIVE.value, Subscription.end_date <= datetime.now(UTC)))
|
||||||
)
|
)
|
||||||
return result.scalars().all()
|
return result.scalars().all()
|
||||||
|
|
||||||
|
|
||||||
async def get_subscriptions_for_autopay(db: AsyncSession) -> list[Subscription]:
|
async def get_subscriptions_for_autopay(db: AsyncSession) -> list[Subscription]:
|
||||||
current_time = datetime.utcnow()
|
current_time = datetime.now(UTC)
|
||||||
|
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(Subscription)
|
select(Subscription)
|
||||||
@@ -811,7 +811,7 @@ async def get_subscriptions_statistics(db: AsyncSession) -> dict:
|
|||||||
|
|
||||||
paid_subscriptions = active_subscriptions - trial_subscriptions
|
paid_subscriptions = active_subscriptions - trial_subscriptions
|
||||||
|
|
||||||
today = datetime.utcnow().date()
|
today = datetime.now(UTC).date()
|
||||||
today_result = await db.execute(
|
today_result = await db.execute(
|
||||||
select(func.count(Subscription.id)).where(
|
select(func.count(Subscription.id)).where(
|
||||||
and_(Subscription.created_at >= today, Subscription.is_trial == False)
|
and_(Subscription.created_at >= today, Subscription.is_trial == False)
|
||||||
@@ -819,7 +819,7 @@ async def get_subscriptions_statistics(db: AsyncSession) -> dict:
|
|||||||
)
|
)
|
||||||
purchased_today = today_result.scalar()
|
purchased_today = today_result.scalar()
|
||||||
|
|
||||||
week_ago = datetime.utcnow() - timedelta(days=7)
|
week_ago = datetime.now(UTC) - timedelta(days=7)
|
||||||
week_result = await db.execute(
|
week_result = await db.execute(
|
||||||
select(func.count(Subscription.id)).where(
|
select(func.count(Subscription.id)).where(
|
||||||
and_(Subscription.created_at >= week_ago, Subscription.is_trial == False)
|
and_(Subscription.created_at >= week_ago, Subscription.is_trial == False)
|
||||||
@@ -827,7 +827,7 @@ async def get_subscriptions_statistics(db: AsyncSession) -> dict:
|
|||||||
)
|
)
|
||||||
purchased_week = week_result.scalar()
|
purchased_week = week_result.scalar()
|
||||||
|
|
||||||
month_ago = datetime.utcnow() - timedelta(days=30)
|
month_ago = datetime.now(UTC) - timedelta(days=30)
|
||||||
month_result = await db.execute(
|
month_result = await db.execute(
|
||||||
select(func.count(Subscription.id)).where(
|
select(func.count(Subscription.id)).where(
|
||||||
and_(Subscription.created_at >= month_ago, Subscription.is_trial == False)
|
and_(Subscription.created_at >= month_ago, Subscription.is_trial == False)
|
||||||
@@ -880,7 +880,7 @@ async def get_subscriptions_statistics(db: AsyncSession) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
async def get_trial_statistics(db: AsyncSession) -> dict:
|
async def get_trial_statistics(db: AsyncSession) -> dict:
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
|
|
||||||
total_trials_result = await db.execute(select(func.count(Subscription.id)).where(Subscription.is_trial.is_(True)))
|
total_trials_result = await db.execute(select(func.count(Subscription.id)).where(Subscription.is_trial.is_(True)))
|
||||||
total_trials = total_trials_result.scalar() or 0
|
total_trials = total_trials_result.scalar() or 0
|
||||||
@@ -913,7 +913,7 @@ async def get_trial_statistics(db: AsyncSession) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
async def reset_trials_for_users_without_paid_subscription(db: AsyncSession) -> int:
|
async def reset_trials_for_users_without_paid_subscription(db: AsyncSession) -> int:
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
|
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(Subscription)
|
select(Subscription)
|
||||||
@@ -970,7 +970,7 @@ async def reset_trials_for_users_without_paid_subscription(db: AsyncSession) ->
|
|||||||
|
|
||||||
async def update_subscription_usage(db: AsyncSession, subscription: Subscription, used_gb: float) -> Subscription:
|
async def update_subscription_usage(db: AsyncSession, subscription: Subscription, used_gb: float) -> Subscription:
|
||||||
subscription.traffic_used_gb = used_gb
|
subscription.traffic_used_gb = used_gb
|
||||||
subscription.updated_at = datetime.utcnow()
|
subscription.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(subscription)
|
await db.refresh(subscription)
|
||||||
@@ -1522,7 +1522,7 @@ async def calculate_addon_cost_for_remaining_period(
|
|||||||
|
|
||||||
async def expire_subscription(db: AsyncSession, subscription: Subscription) -> Subscription:
|
async def expire_subscription(db: AsyncSession, subscription: Subscription) -> Subscription:
|
||||||
subscription.status = SubscriptionStatus.EXPIRED.value
|
subscription.status = SubscriptionStatus.EXPIRED.value
|
||||||
subscription.updated_at = datetime.utcnow()
|
subscription.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(subscription)
|
await db.refresh(subscription)
|
||||||
@@ -1532,7 +1532,7 @@ async def expire_subscription(db: AsyncSession, subscription: Subscription) -> S
|
|||||||
|
|
||||||
|
|
||||||
async def check_and_update_subscription_status(db: AsyncSession, subscription: Subscription) -> Subscription:
|
async def check_and_update_subscription_status(db: AsyncSession, subscription: Subscription) -> Subscription:
|
||||||
current_time = datetime.utcnow()
|
current_time = datetime.now(UTC)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
'🔍 Проверка статуса подписки , текущий статус дата окончания текущее время',
|
'🔍 Проверка статуса подписки , текущий статус дата окончания текущее время',
|
||||||
@@ -1595,7 +1595,7 @@ async def create_subscription_no_commit(
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if end_date is None:
|
if end_date is None:
|
||||||
end_date = datetime.utcnow() + timedelta(days=3)
|
end_date = datetime.now(UTC) + timedelta(days=3)
|
||||||
|
|
||||||
if connected_squads is None:
|
if connected_squads is None:
|
||||||
connected_squads = []
|
connected_squads = []
|
||||||
@@ -1645,7 +1645,7 @@ async def create_subscription(
|
|||||||
autopay_days_before: int | None = None,
|
autopay_days_before: int | None = None,
|
||||||
) -> Subscription:
|
) -> Subscription:
|
||||||
if end_date is None:
|
if end_date is None:
|
||||||
end_date = datetime.utcnow() + timedelta(days=3)
|
end_date = datetime.now(UTC) + timedelta(days=3)
|
||||||
|
|
||||||
if connected_squads is None:
|
if connected_squads is None:
|
||||||
connected_squads = []
|
connected_squads = []
|
||||||
@@ -1693,7 +1693,7 @@ async def create_pending_subscription(
|
|||||||
is_trial: If True, marks the subscription as a trial subscription.
|
is_trial: If True, marks the subscription as a trial subscription.
|
||||||
"""
|
"""
|
||||||
trial_label = 'триальная ' if is_trial else ''
|
trial_label = 'триальная ' if is_trial else ''
|
||||||
current_time = datetime.utcnow()
|
current_time = datetime.now(UTC)
|
||||||
end_date = current_time + timedelta(days=duration_days)
|
end_date = current_time + timedelta(days=duration_days)
|
||||||
|
|
||||||
existing_subscription = await get_subscription_by_user_id(db, user_id)
|
existing_subscription = await get_subscription_by_user_id(db, user_id)
|
||||||
@@ -1809,7 +1809,7 @@ async def activate_pending_subscription(db: AsyncSession, user_id: int, period_d
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Обновляем статус подписки на ACTIVE
|
# Обновляем статус подписки на ACTIVE
|
||||||
current_time = datetime.utcnow()
|
current_time = datetime.now(UTC)
|
||||||
pending_subscription.status = SubscriptionStatus.ACTIVE.value
|
pending_subscription.status = SubscriptionStatus.ACTIVE.value
|
||||||
|
|
||||||
# Если указан период, обновляем дату окончания
|
# Если указан период, обновляем дату окончания
|
||||||
@@ -1870,7 +1870,7 @@ async def activate_pending_trial_subscription(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Обновляем статус подписки на ACTIVE
|
# Обновляем статус подписки на ACTIVE
|
||||||
current_time = datetime.utcnow()
|
current_time = datetime.now(UTC)
|
||||||
pending_subscription.status = SubscriptionStatus.ACTIVE.value
|
pending_subscription.status = SubscriptionStatus.ACTIVE.value
|
||||||
|
|
||||||
# Обновляем даты
|
# Обновляем даты
|
||||||
@@ -1914,7 +1914,7 @@ async def get_daily_subscriptions_for_charge(db: AsyncSession) -> list[Subscript
|
|||||||
"""
|
"""
|
||||||
from app.database.models import Tariff
|
from app.database.models import Tariff
|
||||||
|
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
one_day_ago = now - timedelta(hours=24)
|
one_day_ago = now - timedelta(hours=24)
|
||||||
|
|
||||||
query = (
|
query = (
|
||||||
@@ -2020,8 +2020,8 @@ async def resume_daily_subscription(
|
|||||||
previous_status = subscription.status
|
previous_status = subscription.status
|
||||||
subscription.status = SubscriptionStatus.ACTIVE.value
|
subscription.status = SubscriptionStatus.ACTIVE.value
|
||||||
# Обновляем время последнего списания для корректного расчёта следующего
|
# Обновляем время последнего списания для корректного расчёта следующего
|
||||||
subscription.last_daily_charge_at = datetime.utcnow()
|
subscription.last_daily_charge_at = datetime.now(UTC)
|
||||||
subscription.end_date = datetime.utcnow() + timedelta(days=1)
|
subscription.end_date = datetime.now(UTC) + timedelta(days=1)
|
||||||
logger.info(
|
logger.info(
|
||||||
'✅ Суточная подписка восстановлена из в ACTIVE',
|
'✅ Суточная подписка восстановлена из в ACTIVE',
|
||||||
subscription_id=subscription.id,
|
subscription_id=subscription.id,
|
||||||
@@ -2044,7 +2044,7 @@ async def update_daily_charge_time(
|
|||||||
charge_time: datetime = None,
|
charge_time: datetime = None,
|
||||||
) -> Subscription:
|
) -> Subscription:
|
||||||
"""Обновляет время последнего суточного списания и продлевает подписку на 1 день."""
|
"""Обновляет время последнего суточного списания и продлевает подписку на 1 день."""
|
||||||
now = charge_time or datetime.utcnow()
|
now = charge_time or datetime.now(UTC)
|
||||||
subscription.last_daily_charge_at = now
|
subscription.last_daily_charge_at = now
|
||||||
|
|
||||||
# Продлеваем подписку на 1 день от текущего момента
|
# Продлеваем подписку на 1 день от текущего момента
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from sqlalchemy import func, select
|
from sqlalchemy import func, select
|
||||||
@@ -20,7 +20,7 @@ async def create_subscription_conversion(
|
|||||||
) -> SubscriptionConversion:
|
) -> SubscriptionConversion:
|
||||||
conversion = SubscriptionConversion(
|
conversion = SubscriptionConversion(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
converted_at=datetime.utcnow(),
|
converted_at=datetime.now(UTC),
|
||||||
trial_duration_days=trial_duration_days,
|
trial_duration_days=trial_duration_days,
|
||||||
payment_method=payment_method,
|
payment_method=payment_method,
|
||||||
first_payment_amount_kopeks=first_payment_amount_kopeks,
|
first_payment_amount_kopeks=first_payment_amount_kopeks,
|
||||||
@@ -82,7 +82,7 @@ async def get_conversion_statistics(db: AsyncSession) -> dict:
|
|||||||
avg_first_payment_result = await db.execute(select(func.avg(SubscriptionConversion.first_payment_amount_kopeks)))
|
avg_first_payment_result = await db.execute(select(func.avg(SubscriptionConversion.first_payment_amount_kopeks)))
|
||||||
avg_first_payment = avg_first_payment_result.scalar() or 0
|
avg_first_payment = avg_first_payment_result.scalar() or 0
|
||||||
|
|
||||||
month_ago = datetime.utcnow() - timedelta(days=30)
|
month_ago = datetime.now(UTC) - timedelta(days=30)
|
||||||
month_conversions_result = await db.execute(
|
month_conversions_result = await db.execute(
|
||||||
select(func.count(SubscriptionConversion.id)).where(SubscriptionConversion.converted_at >= month_ago)
|
select(func.count(SubscriptionConversion.id)).where(SubscriptionConversion.converted_at >= month_ago)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from sqlalchemy import and_, func, select
|
from sqlalchemy import and_, func, select
|
||||||
@@ -32,7 +32,7 @@ async def create_subscription_event(
|
|||||||
amount_kopeks=amount_kopeks,
|
amount_kopeks=amount_kopeks,
|
||||||
currency=currency,
|
currency=currency,
|
||||||
message=message,
|
message=message,
|
||||||
occurred_at=occurred_at or datetime.utcnow(),
|
occurred_at=occurred_at or datetime.now(UTC),
|
||||||
extra=extra or None,
|
extra=extra or None,
|
||||||
)
|
)
|
||||||
db.add(event)
|
db.add(event)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from sqlalchemy import and_, desc, func, or_, select, update
|
from sqlalchemy import and_, desc, func, or_, select, update
|
||||||
@@ -154,18 +154,18 @@ class TicketCRUD:
|
|||||||
tickets = result.scalars().all()
|
tickets = result.scalars().all()
|
||||||
if not tickets:
|
if not tickets:
|
||||||
return None
|
return None
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
# Если есть вечная блокировка в любом тикете — блок активен без срока
|
# Если есть вечная блокировка в любом тикете — блок активен без срока
|
||||||
for t in tickets:
|
for t in tickets:
|
||||||
if t.user_reply_block_permanent:
|
if t.user_reply_block_permanent:
|
||||||
return datetime.max
|
return datetime.max.replace(tzinfo=UTC)
|
||||||
# Иначе ищем максимальный срок блокировки, если он в будущем
|
# Иначе ищем максимальный срок блокировки, если он в будущем
|
||||||
future_until = [t.user_reply_block_until for t in tickets if t.user_reply_block_until]
|
future_until = [t.user_reply_block_until for t in tickets if t.user_reply_block_until]
|
||||||
if not future_until:
|
if not future_until:
|
||||||
return None
|
return None
|
||||||
max_until = max(future_until)
|
max_until = max(future_until)
|
||||||
return max_until if max_until > datetime.utcnow() else None
|
return max_until if max_until > datetime.now(UTC) else None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_all_tickets(
|
async def get_all_tickets(
|
||||||
@@ -225,7 +225,7 @@ class TicketCRUD:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
ticket.status = status
|
ticket.status = status
|
||||||
ticket.updated_at = datetime.utcnow()
|
ticket.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
if status == TicketStatus.CLOSED.value and closed_at:
|
if status == TicketStatus.CLOSED.value and closed_at:
|
||||||
ticket.closed_at = closed_at
|
ticket.closed_at = closed_at
|
||||||
@@ -259,14 +259,14 @@ class TicketCRUD:
|
|||||||
return False
|
return False
|
||||||
ticket.user_reply_block_permanent = bool(permanent)
|
ticket.user_reply_block_permanent = bool(permanent)
|
||||||
ticket.user_reply_block_until = until
|
ticket.user_reply_block_until = until
|
||||||
ticket.updated_at = datetime.utcnow()
|
ticket.updated_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def close_ticket(db: AsyncSession, ticket_id: int) -> bool:
|
async def close_ticket(db: AsyncSession, ticket_id: int) -> bool:
|
||||||
"""Закрыть тикет"""
|
"""Закрыть тикет"""
|
||||||
return await TicketCRUD.update_ticket_status(db, ticket_id, TicketStatus.CLOSED.value, datetime.utcnow())
|
return await TicketCRUD.update_ticket_status(db, ticket_id, TicketStatus.CLOSED.value, datetime.now(UTC))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def close_all_open_tickets(
|
async def close_all_open_tickets(
|
||||||
@@ -280,7 +280,7 @@ class TicketCRUD:
|
|||||||
if not ticket_ids:
|
if not ticket_ids:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
await db.execute(
|
await db.execute(
|
||||||
update(Ticket)
|
update(Ticket)
|
||||||
.where(Ticket.id.in_(ticket_ids))
|
.where(Ticket.id.in_(ticket_ids))
|
||||||
@@ -417,7 +417,7 @@ class TicketMessageCRUD:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
ticket.updated_at = datetime.utcnow()
|
ticket.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(message)
|
await db.refresh(message)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""CRUD operations for TicketNotification."""
|
"""CRUD operations for TicketNotification."""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from sqlalchemy import desc, func, select, update
|
from sqlalchemy import desc, func, select, update
|
||||||
@@ -40,7 +40,7 @@ class TicketNotificationCRUD:
|
|||||||
message=message,
|
message=message,
|
||||||
is_for_admin=is_for_admin,
|
is_for_admin=is_for_admin,
|
||||||
is_read=False,
|
is_read=False,
|
||||||
created_at=datetime.utcnow(),
|
created_at=datetime.now(UTC),
|
||||||
)
|
)
|
||||||
db.add(notification)
|
db.add(notification)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
@@ -130,7 +130,7 @@ class TicketNotificationCRUD:
|
|||||||
query = (
|
query = (
|
||||||
update(TicketNotification)
|
update(TicketNotification)
|
||||||
.where(TicketNotification.id == notification_id)
|
.where(TicketNotification.id == notification_id)
|
||||||
.values(is_read=True, read_at=datetime.utcnow())
|
.values(is_read=True, read_at=datetime.now(UTC))
|
||||||
)
|
)
|
||||||
result = await db.execute(query)
|
result = await db.execute(query)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
@@ -146,7 +146,7 @@ class TicketNotificationCRUD:
|
|||||||
TicketNotification.is_for_admin == False,
|
TicketNotification.is_for_admin == False,
|
||||||
TicketNotification.is_read == False,
|
TicketNotification.is_read == False,
|
||||||
)
|
)
|
||||||
.values(is_read=True, read_at=datetime.utcnow())
|
.values(is_read=True, read_at=datetime.now(UTC))
|
||||||
)
|
)
|
||||||
result = await db.execute(query)
|
result = await db.execute(query)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
@@ -161,7 +161,7 @@ class TicketNotificationCRUD:
|
|||||||
TicketNotification.is_for_admin == True,
|
TicketNotification.is_for_admin == True,
|
||||||
TicketNotification.is_read == False,
|
TicketNotification.is_read == False,
|
||||||
)
|
)
|
||||||
.values(is_read=True, read_at=datetime.utcnow())
|
.values(is_read=True, read_at=datetime.now(UTC))
|
||||||
)
|
)
|
||||||
result = await db.execute(query)
|
result = await db.execute(query)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
@@ -178,7 +178,7 @@ class TicketNotificationCRUD:
|
|||||||
TicketNotification.ticket_id == ticket_id,
|
TicketNotification.ticket_id == ticket_id,
|
||||||
TicketNotification.is_read == False,
|
TicketNotification.is_read == False,
|
||||||
)
|
)
|
||||||
.values(is_read=True, read_at=datetime.utcnow())
|
.values(is_read=True, read_at=datetime.now(UTC))
|
||||||
)
|
)
|
||||||
|
|
||||||
if is_admin:
|
if is_admin:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from sqlalchemy import and_, func, or_, select
|
from sqlalchemy import and_, func, or_, select
|
||||||
@@ -47,7 +47,7 @@ async def create_transaction(
|
|||||||
payment_method=payment_method.value if payment_method else None,
|
payment_method=payment_method.value if payment_method else None,
|
||||||
external_id=external_id,
|
external_id=external_id,
|
||||||
is_completed=is_completed,
|
is_completed=is_completed,
|
||||||
completed_at=datetime.utcnow() if is_completed else None,
|
completed_at=datetime.now(UTC) if is_completed else None,
|
||||||
**({'created_at': created_at} if created_at else {}),
|
**({'created_at': created_at} if created_at else {}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ async def get_user_total_spent_kopeks(db: AsyncSession, user_id: int) -> int:
|
|||||||
|
|
||||||
async def complete_transaction(db: AsyncSession, transaction: Transaction) -> Transaction:
|
async def complete_transaction(db: AsyncSession, transaction: Transaction) -> Transaction:
|
||||||
transaction.is_completed = True
|
transaction.is_completed = True
|
||||||
transaction.completed_at = datetime.utcnow()
|
transaction.completed_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(transaction)
|
await db.refresh(transaction)
|
||||||
@@ -198,9 +198,9 @@ async def get_transactions_statistics(
|
|||||||
db: AsyncSession, start_date: datetime | None = None, end_date: datetime | None = None
|
db: AsyncSession, start_date: datetime | None = None, end_date: datetime | None = None
|
||||||
) -> dict:
|
) -> dict:
|
||||||
if not start_date:
|
if not start_date:
|
||||||
start_date = datetime.utcnow().replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
start_date = datetime.now(UTC).replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||||
if not end_date:
|
if not end_date:
|
||||||
end_date = datetime.utcnow()
|
end_date = datetime.now(UTC)
|
||||||
|
|
||||||
# Доход считаем только по реальным платежам (исключаем колесо, промокоды, админские пополнения)
|
# Доход считаем только по реальным платежам (исключаем колесо, промокоды, админские пополнения)
|
||||||
income_result = await db.execute(
|
income_result = await db.execute(
|
||||||
@@ -279,7 +279,7 @@ async def get_transactions_statistics(
|
|||||||
row.payment_method: {'count': row.count, 'amount': row.total_amount} for row in payment_methods_result
|
row.payment_method: {'count': row.count, 'amount': row.total_amount} for row in payment_methods_result
|
||||||
}
|
}
|
||||||
|
|
||||||
today = datetime.utcnow().date()
|
today = datetime.now(UTC).date()
|
||||||
today_result = await db.execute(
|
today_result = await db.execute(
|
||||||
select(func.count(Transaction.id)).where(
|
select(func.count(Transaction.id)).where(
|
||||||
and_(Transaction.is_completed == True, Transaction.created_at >= today)
|
and_(Transaction.is_completed == True, Transaction.created_at >= today)
|
||||||
@@ -316,7 +316,7 @@ async def get_transactions_statistics(
|
|||||||
|
|
||||||
async def get_revenue_by_period(db: AsyncSession, days: int = 30) -> list[dict]:
|
async def get_revenue_by_period(db: AsyncSession, days: int = 30) -> list[dict]:
|
||||||
"""Доход по дням - только реальные платежи."""
|
"""Доход по дням - только реальные платежи."""
|
||||||
start_date = datetime.utcnow() - timedelta(days=days)
|
start_date = datetime.now(UTC) - timedelta(days=days)
|
||||||
|
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(
|
select(
|
||||||
@@ -363,7 +363,7 @@ async def find_tribute_transactions_by_payment_id(
|
|||||||
async def check_tribute_payment_duplicate(
|
async def check_tribute_payment_duplicate(
|
||||||
db: AsyncSession, payment_id: str, amount_kopeks: int, user_telegram_id: int
|
db: AsyncSession, payment_id: str, amount_kopeks: int, user_telegram_id: int
|
||||||
) -> Transaction | None:
|
) -> Transaction | None:
|
||||||
cutoff_time = datetime.utcnow() - timedelta(hours=24)
|
cutoff_time = datetime.now(UTC) - timedelta(hours=24)
|
||||||
|
|
||||||
exact_external_id = f'donation_{payment_id}'
|
exact_external_id = f'donation_{payment_id}'
|
||||||
|
|
||||||
@@ -400,7 +400,7 @@ async def create_unique_tribute_transaction(
|
|||||||
existing = await get_transaction_by_external_id(db, external_id, PaymentMethod.TRIBUTE)
|
existing = await get_transaction_by_external_id(db, external_id, PaymentMethod.TRIBUTE)
|
||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
timestamp = int(datetime.utcnow().timestamp())
|
timestamp = int(datetime.now(UTC).timestamp())
|
||||||
external_id = f'donation_{payment_id}_{amount_kopeks}_{timestamp}'
|
external_id = f'donation_{payment_id}_{amount_kopeks}_{timestamp}'
|
||||||
|
|
||||||
logger.info('Создан уникальный external_id для избежания дубликатов', external_id=external_id)
|
logger.info('Создан уникальный external_id для избежания дубликатов', external_id=external_id)
|
||||||
|
|||||||
+18
-18
@@ -195,7 +195,7 @@ async def create_unique_referral_code(db: AsyncSession) -> str:
|
|||||||
if not existing_user:
|
if not existing_user:
|
||||||
return code
|
return code
|
||||||
|
|
||||||
timestamp = str(int(datetime.utcnow().timestamp()))[-6:]
|
timestamp = str(int(datetime.now(UTC).timestamp()))[-6:]
|
||||||
return f'ref{timestamp}'
|
return f'ref{timestamp}'
|
||||||
|
|
||||||
|
|
||||||
@@ -379,7 +379,7 @@ async def update_user(db: AsyncSession, user: User, **kwargs) -> User:
|
|||||||
if hasattr(user, field):
|
if hasattr(user, field):
|
||||||
setattr(user, field, value)
|
setattr(user, field, value)
|
||||||
|
|
||||||
user.updated_at = datetime.utcnow()
|
user.updated_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(user)
|
await db.refresh(user)
|
||||||
|
|
||||||
@@ -399,7 +399,7 @@ async def add_user_balance(
|
|||||||
try:
|
try:
|
||||||
old_balance = user.balance_kopeks
|
old_balance = user.balance_kopeks
|
||||||
user.balance_kopeks += amount_kopeks
|
user.balance_kopeks += amount_kopeks
|
||||||
user.updated_at = datetime.utcnow()
|
user.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
if create_transaction:
|
if create_transaction:
|
||||||
from app.database.crud.transaction import create_transaction as create_trans
|
from app.database.crud.transaction import create_transaction as create_trans
|
||||||
@@ -564,7 +564,7 @@ async def subtract_user_balance(
|
|||||||
user.promo_offer_discount_source = None
|
user.promo_offer_discount_source = None
|
||||||
user.promo_offer_discount_expires_at = None
|
user.promo_offer_discount_expires_at = None
|
||||||
|
|
||||||
user.updated_at = datetime.utcnow()
|
user.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
if create_transaction:
|
if create_transaction:
|
||||||
from app.database.crud.transaction import (
|
from app.database.crud.transaction import (
|
||||||
@@ -620,7 +620,7 @@ async def subtract_user_balance(
|
|||||||
|
|
||||||
|
|
||||||
async def cleanup_expired_promo_offer_discounts(db: AsyncSession) -> int:
|
async def cleanup_expired_promo_offer_discounts(db: AsyncSession) -> int:
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(User).where(
|
select(User).where(
|
||||||
User.promo_offer_discount_percent > 0,
|
User.promo_offer_discount_percent > 0,
|
||||||
@@ -899,7 +899,7 @@ async def get_referrals(db: AsyncSession, user_id: int) -> list[User]:
|
|||||||
|
|
||||||
|
|
||||||
async def get_users_for_promo_segment(db: AsyncSession, segment: str) -> list[User]:
|
async def get_users_for_promo_segment(db: AsyncSession, segment: str) -> list[User]:
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
|
|
||||||
base_query = (
|
base_query = (
|
||||||
select(User)
|
select(User)
|
||||||
@@ -961,7 +961,7 @@ async def get_users_for_promo_segment(db: AsyncSession, segment: str) -> list[Us
|
|||||||
|
|
||||||
|
|
||||||
async def get_inactive_users(db: AsyncSession, months: int = 3) -> list[User]:
|
async def get_inactive_users(db: AsyncSession, months: int = 3) -> list[User]:
|
||||||
threshold_date = datetime.utcnow() - timedelta(days=months * 30)
|
threshold_date = datetime.now(UTC) - timedelta(days=months * 30)
|
||||||
|
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(User)
|
select(User)
|
||||||
@@ -986,7 +986,7 @@ async def get_inactive_users(db: AsyncSession, months: int = 3) -> list[User]:
|
|||||||
|
|
||||||
async def delete_user(db: AsyncSession, user: User) -> bool:
|
async def delete_user(db: AsyncSession, user: User) -> bool:
|
||||||
user.status = UserStatus.DELETED.value
|
user.status = UserStatus.DELETED.value
|
||||||
user.updated_at = datetime.utcnow()
|
user.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
user_id_display = user.telegram_id or user.email or f'#{user.id}'
|
user_id_display = user.telegram_id or user.email or f'#{user.id}'
|
||||||
@@ -1001,19 +1001,19 @@ async def get_users_statistics(db: AsyncSession) -> dict:
|
|||||||
active_result = await db.execute(select(func.count(User.id)).where(User.status == UserStatus.ACTIVE.value))
|
active_result = await db.execute(select(func.count(User.id)).where(User.status == UserStatus.ACTIVE.value))
|
||||||
active_users = active_result.scalar()
|
active_users = active_result.scalar()
|
||||||
|
|
||||||
today = datetime.utcnow().date()
|
today = datetime.now(UTC).date()
|
||||||
today_result = await db.execute(
|
today_result = await db.execute(
|
||||||
select(func.count(User.id)).where(and_(User.created_at >= today, User.status == UserStatus.ACTIVE.value))
|
select(func.count(User.id)).where(and_(User.created_at >= today, User.status == UserStatus.ACTIVE.value))
|
||||||
)
|
)
|
||||||
new_today = today_result.scalar()
|
new_today = today_result.scalar()
|
||||||
|
|
||||||
week_ago = datetime.utcnow() - timedelta(days=7)
|
week_ago = datetime.now(UTC) - timedelta(days=7)
|
||||||
week_result = await db.execute(
|
week_result = await db.execute(
|
||||||
select(func.count(User.id)).where(and_(User.created_at >= week_ago, User.status == UserStatus.ACTIVE.value))
|
select(func.count(User.id)).where(and_(User.created_at >= week_ago, User.status == UserStatus.ACTIVE.value))
|
||||||
)
|
)
|
||||||
new_week = week_result.scalar()
|
new_week = week_result.scalar()
|
||||||
|
|
||||||
month_ago = datetime.utcnow() - timedelta(days=30)
|
month_ago = datetime.now(UTC) - timedelta(days=30)
|
||||||
month_result = await db.execute(
|
month_result = await db.execute(
|
||||||
select(func.count(User.id)).where(and_(User.created_at >= month_ago, User.status == UserStatus.ACTIVE.value))
|
select(func.count(User.id)).where(and_(User.created_at >= month_ago, User.status == UserStatus.ACTIVE.value))
|
||||||
)
|
)
|
||||||
@@ -1037,7 +1037,7 @@ async def get_users_with_active_subscriptions(db: AsyncSession) -> list[User]:
|
|||||||
Returns:
|
Returns:
|
||||||
Список пользователей с активными подписками и remnawave_uuid
|
Список пользователей с активными подписками и remnawave_uuid
|
||||||
"""
|
"""
|
||||||
current_time = datetime.utcnow()
|
current_time = datetime.now(UTC)
|
||||||
|
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(User)
|
select(User)
|
||||||
@@ -1177,7 +1177,7 @@ async def set_email_change_pending(
|
|||||||
user.email_change_new = new_email
|
user.email_change_new = new_email
|
||||||
user.email_change_code = code
|
user.email_change_code = code
|
||||||
user.email_change_expires = expires_at
|
user.email_change_expires = expires_at
|
||||||
user.updated_at = datetime.utcnow()
|
user.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(user)
|
await db.refresh(user)
|
||||||
@@ -1201,7 +1201,7 @@ async def verify_and_apply_email_change(db: AsyncSession, user: User, code: str)
|
|||||||
if not user.email_change_new or not user.email_change_code:
|
if not user.email_change_new or not user.email_change_code:
|
||||||
return False, 'No pending email change'
|
return False, 'No pending email change'
|
||||||
|
|
||||||
if user.email_change_expires and datetime.utcnow() > user.email_change_expires:
|
if user.email_change_expires and datetime.now(UTC) > user.email_change_expires:
|
||||||
# Clear expired data
|
# Clear expired data
|
||||||
user.email_change_new = None
|
user.email_change_new = None
|
||||||
user.email_change_code = None
|
user.email_change_code = None
|
||||||
@@ -1227,11 +1227,11 @@ async def verify_and_apply_email_change(db: AsyncSession, user: User, code: str)
|
|||||||
# Apply the change
|
# Apply the change
|
||||||
user.email = new_email
|
user.email = new_email
|
||||||
user.email_verified = True
|
user.email_verified = True
|
||||||
user.email_verified_at = datetime.utcnow()
|
user.email_verified_at = datetime.now(UTC)
|
||||||
user.email_change_new = None
|
user.email_change_new = None
|
||||||
user.email_change_code = None
|
user.email_change_code = None
|
||||||
user.email_change_expires = None
|
user.email_change_expires = None
|
||||||
user.updated_at = datetime.utcnow()
|
user.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(user)
|
await db.refresh(user)
|
||||||
@@ -1251,7 +1251,7 @@ async def clear_email_change_pending(db: AsyncSession, user: User) -> None:
|
|||||||
user.email_change_new = None
|
user.email_change_new = None
|
||||||
user.email_change_code = None
|
user.email_change_code = None
|
||||||
user.email_change_expires = None
|
user.email_change_expires = None
|
||||||
user.updated_at = datetime.utcnow()
|
user.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
logger.info('Email change cancelled for user', user_id=user.id)
|
logger.info('Email change cancelled for user', user_id=user.id)
|
||||||
@@ -1286,7 +1286,7 @@ async def set_user_oauth_provider_id(db: AsyncSession, user: User, provider: str
|
|||||||
return
|
return
|
||||||
value: str | int = int(provider_id) if provider == 'vk' else provider_id
|
value: str | int = int(provider_id) if provider == 'vk' else provider_id
|
||||||
setattr(user, column_name, value)
|
setattr(user, column_name, value)
|
||||||
user.updated_at = datetime.now(UTC).replace(tzinfo=None)
|
user.updated_at = datetime.now(UTC)
|
||||||
logger.info('Linked (id=) to user', provider=provider, provider_id=provider_id, user_id=user.id)
|
logger.info('Linked (id=) to user', provider=provider, provider_id=provider_id, user_id=user.id)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import random
|
import random
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from sqlalchemy import func, select
|
from sqlalchemy import func, select
|
||||||
@@ -111,7 +111,7 @@ async def update_user_message(
|
|||||||
if sort_order is not None:
|
if sort_order is not None:
|
||||||
message.sort_order = sort_order
|
message.sort_order = sort_order
|
||||||
|
|
||||||
message.updated_at = datetime.utcnow()
|
message.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(message)
|
await db.refresh(message)
|
||||||
@@ -127,7 +127,7 @@ async def toggle_user_message_status(db: AsyncSession, message_id: int) -> UserM
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
message.is_active = not message.is_active
|
message.is_active = not message.is_active
|
||||||
message.updated_at = datetime.utcnow()
|
message.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(message)
|
await db.refresh(message)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""CRUD операции для связи пользователей с промогруппами (Many-to-Many)."""
|
"""CRUD операции для связи пользователей с промогруппами (Many-to-Many)."""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from sqlalchemy import and_, desc, select
|
from sqlalchemy import and_, desc, select
|
||||||
@@ -36,7 +36,7 @@ async def _sync_user_primary_promo_group(
|
|||||||
|
|
||||||
if user.promo_group_id != new_primary_id:
|
if user.promo_group_id != new_primary_id:
|
||||||
user.promo_group_id = new_primary_id
|
user.promo_group_id = new_primary_id
|
||||||
user.updated_at = datetime.utcnow()
|
user.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
logger.error('Ошибка синхронизации primary промогруппы пользователя', user_id=user_id, error=error)
|
logger.error('Ошибка синхронизации primary промогруппы пользователя', user_id=user_id, error=error)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
from sqlalchemy import select, update
|
from sqlalchemy import select, update
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
@@ -71,7 +71,7 @@ async def update_token(
|
|||||||
for key, value in kwargs.items():
|
for key, value in kwargs.items():
|
||||||
if hasattr(token, key):
|
if hasattr(token, key):
|
||||||
setattr(token, key, value)
|
setattr(token, key, value)
|
||||||
token.updated_at = datetime.utcnow()
|
token.updated_at = datetime.now(UTC)
|
||||||
await db.flush()
|
await db.flush()
|
||||||
await db.refresh(token)
|
await db.refresh(token)
|
||||||
return token
|
return token
|
||||||
@@ -86,7 +86,7 @@ async def set_tokens_active_status(
|
|||||||
await db.execute(
|
await db.execute(
|
||||||
update(WebApiToken)
|
update(WebApiToken)
|
||||||
.where(WebApiToken.id.in_(list(token_ids)))
|
.where(WebApiToken.id.in_(list(token_ids)))
|
||||||
.values(is_active=is_active, updated_at=datetime.utcnow())
|
.values(is_active=is_active, updated_at=datetime.now(UTC))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
from sqlalchemy import func, select
|
from sqlalchemy import func, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
@@ -94,7 +94,7 @@ async def update_webhook(
|
|||||||
if is_active is not None:
|
if is_active is not None:
|
||||||
webhook.is_active = is_active
|
webhook.is_active = is_active
|
||||||
|
|
||||||
webhook.updated_at = datetime.utcnow()
|
webhook.updated_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(webhook)
|
await db.refresh(webhook)
|
||||||
return webhook
|
return webhook
|
||||||
@@ -127,7 +127,7 @@ async def record_webhook_delivery(
|
|||||||
response_body=response_body,
|
response_body=response_body,
|
||||||
error_message=error_message,
|
error_message=error_message,
|
||||||
attempt_number=attempt_number,
|
attempt_number=attempt_number,
|
||||||
delivered_at=datetime.utcnow() if status == 'success' else None,
|
delivered_at=datetime.now(UTC) if status == 'success' else None,
|
||||||
)
|
)
|
||||||
db.add(delivery)
|
db.add(delivery)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
@@ -145,7 +145,7 @@ async def update_webhook_stats(
|
|||||||
webhook.success_count += 1
|
webhook.success_count += 1
|
||||||
else:
|
else:
|
||||||
webhook.failure_count += 1
|
webhook.failure_count += 1
|
||||||
webhook.last_triggered_at = datetime.utcnow()
|
webhook.last_triggered_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(webhook)
|
await db.refresh(webhook)
|
||||||
return webhook
|
return webhook
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from sqlalchemy import func, select, update
|
from sqlalchemy import func, select, update
|
||||||
@@ -77,7 +77,7 @@ async def toggle_welcome_text_status(db: AsyncSession, admin_id: int) -> bool:
|
|||||||
|
|
||||||
if welcome_text:
|
if welcome_text:
|
||||||
welcome_text.is_enabled = not welcome_text.is_enabled
|
welcome_text.is_enabled = not welcome_text.is_enabled
|
||||||
welcome_text.updated_at = datetime.utcnow()
|
welcome_text.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(welcome_text)
|
await db.refresh(welcome_text)
|
||||||
@@ -182,7 +182,7 @@ async def update_welcome_text(
|
|||||||
if is_active is not None:
|
if is_active is not None:
|
||||||
welcome_text.is_active = is_active
|
welcome_text.is_active = is_active
|
||||||
|
|
||||||
welcome_text.updated_at = datetime.utcnow()
|
welcome_text.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(welcome_text)
|
await db.refresh(welcome_text)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
CRUD операции для колеса удачи (Fortune Wheel).
|
CRUD операции для колеса удачи (Fortune Wheel).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
@@ -65,7 +65,7 @@ async def update_wheel_config(db: AsyncSession, **kwargs) -> WheelConfig:
|
|||||||
if hasattr(config, key) and value is not None:
|
if hasattr(config, key) and value is not None:
|
||||||
setattr(config, key, value)
|
setattr(config, key, value)
|
||||||
|
|
||||||
config.updated_at = datetime.utcnow()
|
config.updated_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(config)
|
await db.refresh(config)
|
||||||
logger.info('🎡 Обновлена конфигурация колеса', kwargs=kwargs)
|
logger.info('🎡 Обновлена конфигурация колеса', kwargs=kwargs)
|
||||||
@@ -143,7 +143,7 @@ async def update_wheel_prize(db: AsyncSession, prize_id: int, **kwargs) -> Wheel
|
|||||||
if hasattr(prize, key) and value is not None:
|
if hasattr(prize, key) and value is not None:
|
||||||
setattr(prize, key, value)
|
setattr(prize, key, value)
|
||||||
|
|
||||||
prize.updated_at = datetime.utcnow()
|
prize.updated_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(prize)
|
await db.refresh(prize)
|
||||||
logger.info('🎁 Обновлен приз колеса ID', prize_id=prize_id, kwargs=kwargs)
|
logger.info('🎁 Обновлен приз колеса ID', prize_id=prize_id, kwargs=kwargs)
|
||||||
@@ -204,7 +204,7 @@ async def create_wheel_spin(
|
|||||||
prize_value_kopeks=prize_value_kopeks,
|
prize_value_kopeks=prize_value_kopeks,
|
||||||
generated_promocode_id=generated_promocode_id,
|
generated_promocode_id=generated_promocode_id,
|
||||||
is_applied=is_applied,
|
is_applied=is_applied,
|
||||||
applied_at=datetime.utcnow() if is_applied else None,
|
applied_at=datetime.now(UTC) if is_applied else None,
|
||||||
)
|
)
|
||||||
db.add(spin)
|
db.add(spin)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
@@ -219,7 +219,7 @@ async def mark_spin_applied(db: AsyncSession, spin_id: int) -> WheelSpin | None:
|
|||||||
spin = result.scalar_one_or_none()
|
spin = result.scalar_one_or_none()
|
||||||
if spin:
|
if spin:
|
||||||
spin.is_applied = True
|
spin.is_applied = True
|
||||||
spin.applied_at = datetime.utcnow()
|
spin.applied_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(spin)
|
await db.refresh(spin)
|
||||||
return spin
|
return spin
|
||||||
@@ -227,7 +227,7 @@ async def mark_spin_applied(db: AsyncSession, spin_id: int) -> WheelSpin | None:
|
|||||||
|
|
||||||
async def get_user_spins_today(db: AsyncSession, user_id: int) -> int:
|
async def get_user_spins_today(db: AsyncSession, user_id: int) -> int:
|
||||||
"""Получить количество спинов пользователя за сегодня."""
|
"""Получить количество спинов пользователя за сегодня."""
|
||||||
today_start = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
|
today_start = datetime.now(UTC).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(func.count(WheelSpin.id)).where(
|
select(func.count(WheelSpin.id)).where(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from sqlalchemy import and_, select, update
|
from sqlalchemy import and_, select, update
|
||||||
@@ -88,7 +88,7 @@ async def update_yookassa_payment_status(
|
|||||||
captured_at: datetime | None = None,
|
captured_at: datetime | None = None,
|
||||||
payment_method_type: str | None = None,
|
payment_method_type: str | None = None,
|
||||||
) -> YooKassaPayment | None:
|
) -> YooKassaPayment | None:
|
||||||
update_data = {'status': status, 'is_paid': is_paid, 'is_captured': is_captured, 'updated_at': datetime.utcnow()}
|
update_data = {'status': status, 'is_paid': is_paid, 'is_captured': is_captured, 'updated_at': datetime.now(UTC)}
|
||||||
|
|
||||||
if captured_at:
|
if captured_at:
|
||||||
update_data['captured_at'] = captured_at
|
update_data['captured_at'] = captured_at
|
||||||
@@ -125,7 +125,7 @@ async def link_yookassa_payment_to_transaction(
|
|||||||
await db.execute(
|
await db.execute(
|
||||||
update(YooKassaPayment)
|
update(YooKassaPayment)
|
||||||
.where(YooKassaPayment.yookassa_payment_id == yookassa_payment_id)
|
.where(YooKassaPayment.yookassa_payment_id == yookassa_payment_id)
|
||||||
.values(transaction_id=transaction_id, updated_at=datetime.utcnow())
|
.values(transaction_id=transaction_id, updated_at=datetime.now(UTC))
|
||||||
)
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
|
|||||||
+184
-185
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime, time, timedelta
|
from datetime import UTC, datetime, time, timedelta
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
@@ -176,10 +176,10 @@ class YooKassaPayment(Base):
|
|||||||
payment_method_type = Column(String(50), nullable=True)
|
payment_method_type = Column(String(50), nullable=True)
|
||||||
refundable = Column(Boolean, default=False)
|
refundable = Column(Boolean, default=False)
|
||||||
test_mode = Column(Boolean, default=False)
|
test_mode = Column(Boolean, default=False)
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
yookassa_created_at = Column(DateTime, nullable=True)
|
yookassa_created_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
captured_at = Column(DateTime, nullable=True)
|
captured_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
user = relationship('User', backref='yookassa_payments')
|
user = relationship('User', backref='yookassa_payments')
|
||||||
transaction = relationship('Transaction', backref='yookassa_payment')
|
transaction = relationship('Transaction', backref='yookassa_payment')
|
||||||
|
|
||||||
@@ -225,11 +225,11 @@ class CryptoBotPayment(Base):
|
|||||||
mini_app_invoice_url = Column(Text, nullable=True)
|
mini_app_invoice_url = Column(Text, nullable=True)
|
||||||
web_app_invoice_url = Column(Text, nullable=True)
|
web_app_invoice_url = Column(Text, nullable=True)
|
||||||
|
|
||||||
paid_at = Column(DateTime, nullable=True)
|
paid_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
|
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
|
||||||
|
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
user = relationship('User', backref='cryptobot_payments')
|
user = relationship('User', backref='cryptobot_payments')
|
||||||
transaction = relationship('Transaction', backref='cryptobot_payment')
|
transaction = relationship('Transaction', backref='cryptobot_payment')
|
||||||
@@ -277,12 +277,12 @@ class HeleketPayment(Base):
|
|||||||
payment_url = Column(Text, nullable=True)
|
payment_url = Column(Text, nullable=True)
|
||||||
metadata_json = Column(JSON, nullable=True)
|
metadata_json = Column(JSON, nullable=True)
|
||||||
|
|
||||||
paid_at = Column(DateTime, nullable=True)
|
paid_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
expires_at = Column(DateTime, nullable=True)
|
expires_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
|
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
|
||||||
|
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
user = relationship('User', backref='heleket_payments')
|
user = relationship('User', backref='heleket_payments')
|
||||||
transaction = relationship('Transaction', backref='heleket_payment')
|
transaction = relationship('Transaction', backref='heleket_payment')
|
||||||
@@ -330,7 +330,7 @@ class MulenPayPayment(Base):
|
|||||||
|
|
||||||
status = Column(String(50), nullable=False, default='created')
|
status = Column(String(50), nullable=False, default='created')
|
||||||
is_paid = Column(Boolean, default=False)
|
is_paid = Column(Boolean, default=False)
|
||||||
paid_at = Column(DateTime, nullable=True)
|
paid_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
payment_url = Column(Text, nullable=True)
|
payment_url = Column(Text, nullable=True)
|
||||||
metadata_json = Column(JSON, nullable=True)
|
metadata_json = Column(JSON, nullable=True)
|
||||||
@@ -338,8 +338,8 @@ class MulenPayPayment(Base):
|
|||||||
|
|
||||||
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
|
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
|
||||||
|
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
user = relationship('User', backref='mulenpay_payments')
|
user = relationship('User', backref='mulenpay_payments')
|
||||||
transaction = relationship('Transaction', backref='mulenpay_payment')
|
transaction = relationship('Transaction', backref='mulenpay_payment')
|
||||||
@@ -368,9 +368,9 @@ class Pal24Payment(Base):
|
|||||||
status = Column(String(50), nullable=False, default='NEW')
|
status = Column(String(50), nullable=False, default='NEW')
|
||||||
is_active = Column(Boolean, default=True)
|
is_active = Column(Boolean, default=True)
|
||||||
is_paid = Column(Boolean, default=False)
|
is_paid = Column(Boolean, default=False)
|
||||||
paid_at = Column(DateTime, nullable=True)
|
paid_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
last_status = Column(String(50), nullable=True)
|
last_status = Column(String(50), nullable=True)
|
||||||
last_status_checked_at = Column(DateTime, nullable=True)
|
last_status_checked_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
link_url = Column(Text, nullable=True)
|
link_url = Column(Text, nullable=True)
|
||||||
link_page_url = Column(Text, nullable=True)
|
link_page_url = Column(Text, nullable=True)
|
||||||
@@ -385,12 +385,12 @@ class Pal24Payment(Base):
|
|||||||
payer_account = Column(String(255), nullable=True)
|
payer_account = Column(String(255), nullable=True)
|
||||||
|
|
||||||
ttl = Column(Integer, nullable=True)
|
ttl = Column(Integer, nullable=True)
|
||||||
expires_at = Column(DateTime, nullable=True)
|
expires_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
|
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
|
||||||
|
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
user = relationship('User', backref='pal24_payments')
|
user = relationship('User', backref='pal24_payments')
|
||||||
transaction = relationship('Transaction', backref='pal24_payment')
|
transaction = relationship('Transaction', backref='pal24_payment')
|
||||||
@@ -424,7 +424,7 @@ class WataPayment(Base):
|
|||||||
|
|
||||||
status = Column(String(50), nullable=False, default='Opened')
|
status = Column(String(50), nullable=False, default='Opened')
|
||||||
is_paid = Column(Boolean, default=False)
|
is_paid = Column(Boolean, default=False)
|
||||||
paid_at = Column(DateTime, nullable=True)
|
paid_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
last_status = Column(String(50), nullable=True)
|
last_status = Column(String(50), nullable=True)
|
||||||
terminal_public_id = Column(String(64), nullable=True)
|
terminal_public_id = Column(String(64), nullable=True)
|
||||||
|
|
||||||
@@ -434,12 +434,12 @@ class WataPayment(Base):
|
|||||||
metadata_json = Column(JSON, nullable=True)
|
metadata_json = Column(JSON, nullable=True)
|
||||||
callback_payload = Column(JSON, nullable=True)
|
callback_payload = Column(JSON, nullable=True)
|
||||||
|
|
||||||
expires_at = Column(DateTime, nullable=True)
|
expires_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
|
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
|
||||||
|
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
user = relationship('User', backref='wata_payments')
|
user = relationship('User', backref='wata_payments')
|
||||||
transaction = relationship('Transaction', backref='wata_payment')
|
transaction = relationship('Transaction', backref='wata_payment')
|
||||||
@@ -467,7 +467,7 @@ class PlategaPayment(Base):
|
|||||||
payment_method_code = Column(Integer, nullable=False)
|
payment_method_code = Column(Integer, nullable=False)
|
||||||
status = Column(String(50), nullable=False, default='PENDING')
|
status = Column(String(50), nullable=False, default='PENDING')
|
||||||
is_paid = Column(Boolean, default=False)
|
is_paid = Column(Boolean, default=False)
|
||||||
paid_at = Column(DateTime, nullable=True)
|
paid_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
redirect_url = Column(Text, nullable=True)
|
redirect_url = Column(Text, nullable=True)
|
||||||
return_url = Column(Text, nullable=True)
|
return_url = Column(Text, nullable=True)
|
||||||
@@ -476,12 +476,12 @@ class PlategaPayment(Base):
|
|||||||
metadata_json = Column(JSON, nullable=True)
|
metadata_json = Column(JSON, nullable=True)
|
||||||
callback_payload = Column(JSON, nullable=True)
|
callback_payload = Column(JSON, nullable=True)
|
||||||
|
|
||||||
expires_at = Column(DateTime, nullable=True)
|
expires_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
|
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
|
||||||
|
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
user = relationship('User', backref='platega_payments')
|
user = relationship('User', backref='platega_payments')
|
||||||
transaction = relationship('Transaction', backref='platega_payment')
|
transaction = relationship('Transaction', backref='platega_payment')
|
||||||
@@ -510,7 +510,7 @@ class CloudPaymentsPayment(Base):
|
|||||||
|
|
||||||
status = Column(String(50), nullable=False, default='pending') # pending, completed, failed, authorized
|
status = Column(String(50), nullable=False, default='pending') # pending, completed, failed, authorized
|
||||||
is_paid = Column(Boolean, default=False)
|
is_paid = Column(Boolean, default=False)
|
||||||
paid_at = Column(DateTime, nullable=True)
|
paid_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
# Данные карты (маскированные)
|
# Данные карты (маскированные)
|
||||||
card_first_six = Column(String(6), nullable=True)
|
card_first_six = Column(String(6), nullable=True)
|
||||||
@@ -537,8 +537,8 @@ class CloudPaymentsPayment(Base):
|
|||||||
# Связь с транзакцией в нашей системе
|
# Связь с транзакцией в нашей системе
|
||||||
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
|
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
|
||||||
|
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
user = relationship('User', backref='cloudpayments_payments')
|
user = relationship('User', backref='cloudpayments_payments')
|
||||||
transaction = relationship('Transaction', backref='cloudpayments_payment')
|
transaction = relationship('Transaction', backref='cloudpayments_payment')
|
||||||
@@ -591,10 +591,10 @@ class FreekassaPayment(Base):
|
|||||||
callback_payload = Column(JSON, nullable=True)
|
callback_payload = Column(JSON, nullable=True)
|
||||||
|
|
||||||
# Временные метки
|
# Временные метки
|
||||||
paid_at = Column(DateTime, nullable=True)
|
paid_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
expires_at = Column(DateTime, nullable=True)
|
expires_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
# Связь с транзакцией
|
# Связь с транзакцией
|
||||||
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
|
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
|
||||||
@@ -653,10 +653,10 @@ class KassaAiPayment(Base):
|
|||||||
callback_payload = Column(JSON, nullable=True)
|
callback_payload = Column(JSON, nullable=True)
|
||||||
|
|
||||||
# Временные метки
|
# Временные метки
|
||||||
paid_at = Column(DateTime, nullable=True)
|
paid_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
expires_at = Column(DateTime, nullable=True)
|
expires_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
# Связь с транзакцией
|
# Связь с транзакцией
|
||||||
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
|
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
|
||||||
@@ -698,8 +698,8 @@ class PromoGroup(Base):
|
|||||||
auto_assign_total_spent_kopeks = Column(Integer, nullable=True, default=None)
|
auto_assign_total_spent_kopeks = Column(Integer, nullable=True, default=None)
|
||||||
apply_discounts_to_addons = Column(Boolean, nullable=False, default=True)
|
apply_discounts_to_addons = Column(Boolean, nullable=False, default=True)
|
||||||
is_default = Column(Boolean, nullable=False, default=False)
|
is_default = Column(Boolean, nullable=False, default=False)
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
users = relationship('User', back_populates='promo_group')
|
users = relationship('User', back_populates='promo_group')
|
||||||
user_promo_groups = relationship('UserPromoGroup', back_populates='promo_group', cascade='all, delete-orphan')
|
user_promo_groups = relationship('UserPromoGroup', back_populates='promo_group', cascade='all, delete-orphan')
|
||||||
@@ -777,7 +777,7 @@ class UserPromoGroup(Base):
|
|||||||
|
|
||||||
user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), primary_key=True)
|
user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), primary_key=True)
|
||||||
promo_group_id = Column(Integer, ForeignKey('promo_groups.id', ondelete='CASCADE'), primary_key=True)
|
promo_group_id = Column(Integer, ForeignKey('promo_groups.id', ondelete='CASCADE'), primary_key=True)
|
||||||
assigned_at = Column(DateTime, default=func.now())
|
assigned_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
assigned_by = Column(String(50), default='system')
|
assigned_by = Column(String(50), default='system')
|
||||||
|
|
||||||
user = relationship('User', back_populates='user_promo_groups')
|
user = relationship('User', back_populates='user_promo_groups')
|
||||||
@@ -851,8 +851,8 @@ class Tariff(Base):
|
|||||||
# Режим сброса трафика: DAY, WEEK, MONTH, NO_RESET (по умолчанию берётся из конфига)
|
# Режим сброса трафика: DAY, WEEK, MONTH, NO_RESET (по умолчанию берётся из конфига)
|
||||||
traffic_reset_mode = Column(String(20), nullable=True, default=None) # None = использовать глобальную настройку
|
traffic_reset_mode = Column(String(20), nullable=True, default=None) # None = использовать глобальную настройку
|
||||||
|
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
# M2M связь с промогруппами (какие промогруппы имеют доступ к тарифу)
|
# M2M связь с промогруппами (какие промогруппы имеют доступ к тарифу)
|
||||||
allowed_promo_groups = relationship(
|
allowed_promo_groups = relationship(
|
||||||
@@ -976,25 +976,25 @@ class User(Base):
|
|||||||
has_had_paid_subscription = Column(Boolean, default=False, nullable=False)
|
has_had_paid_subscription = Column(Boolean, default=False, nullable=False)
|
||||||
referred_by_id = Column(Integer, ForeignKey('users.id'), nullable=True)
|
referred_by_id = Column(Integer, ForeignKey('users.id'), nullable=True)
|
||||||
referral_code = Column(String(20), unique=True, nullable=True)
|
referral_code = Column(String(20), unique=True, nullable=True)
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
last_activity = Column(DateTime, default=func.now())
|
last_activity = Column(DateTime(timezone=True), default=func.now())
|
||||||
remnawave_uuid = Column(String(255), nullable=True, unique=True)
|
remnawave_uuid = Column(String(255), nullable=True, unique=True)
|
||||||
|
|
||||||
# Cabinet authentication fields
|
# Cabinet authentication fields
|
||||||
email = Column(String(255), unique=True, nullable=True, index=True)
|
email = Column(String(255), unique=True, nullable=True, index=True)
|
||||||
email_verified = Column(Boolean, default=False, nullable=False)
|
email_verified = Column(Boolean, default=False, nullable=False)
|
||||||
email_verified_at = Column(DateTime, nullable=True)
|
email_verified_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
password_hash = Column(String(255), nullable=True)
|
password_hash = Column(String(255), nullable=True)
|
||||||
email_verification_token = Column(String(255), nullable=True)
|
email_verification_token = Column(String(255), nullable=True)
|
||||||
email_verification_expires = Column(DateTime, nullable=True)
|
email_verification_expires = Column(DateTime(timezone=True), nullable=True)
|
||||||
password_reset_token = Column(String(255), nullable=True)
|
password_reset_token = Column(String(255), nullable=True)
|
||||||
password_reset_expires = Column(DateTime, nullable=True)
|
password_reset_expires = Column(DateTime(timezone=True), nullable=True)
|
||||||
cabinet_last_login = Column(DateTime, nullable=True)
|
cabinet_last_login = Column(DateTime(timezone=True), nullable=True)
|
||||||
# Email change fields
|
# Email change fields
|
||||||
email_change_new = Column(String(255), nullable=True) # New email pending verification
|
email_change_new = Column(String(255), nullable=True) # New email pending verification
|
||||||
email_change_code = Column(String(6), nullable=True) # 6-digit verification code
|
email_change_code = Column(String(6), nullable=True) # 6-digit verification code
|
||||||
email_change_expires = Column(DateTime, nullable=True) # Code expiration
|
email_change_expires = Column(DateTime(timezone=True), nullable=True) # Code expiration
|
||||||
# OAuth provider IDs
|
# OAuth provider IDs
|
||||||
google_id = Column(String(255), unique=True, nullable=True, index=True)
|
google_id = Column(String(255), unique=True, nullable=True, index=True)
|
||||||
yandex_id = Column(String(255), unique=True, nullable=True, index=True)
|
yandex_id = Column(String(255), unique=True, nullable=True, index=True)
|
||||||
@@ -1013,8 +1013,8 @@ class User(Base):
|
|||||||
referral_commission_percent = Column(Integer, nullable=True)
|
referral_commission_percent = Column(Integer, nullable=True)
|
||||||
promo_offer_discount_percent = Column(Integer, nullable=False, default=0)
|
promo_offer_discount_percent = Column(Integer, nullable=False, default=0)
|
||||||
promo_offer_discount_source = Column(String(100), nullable=True)
|
promo_offer_discount_source = Column(String(100), nullable=True)
|
||||||
promo_offer_discount_expires_at = Column(DateTime, nullable=True)
|
promo_offer_discount_expires_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
last_remnawave_sync = Column(DateTime, nullable=True)
|
last_remnawave_sync = Column(DateTime(timezone=True), nullable=True)
|
||||||
trojan_password = Column(String(255), nullable=True)
|
trojan_password = Column(String(255), nullable=True)
|
||||||
vless_uuid = Column(String(255), nullable=True)
|
vless_uuid = Column(String(255), nullable=True)
|
||||||
ss_password = Column(String(255), nullable=True)
|
ss_password = Column(String(255), nullable=True)
|
||||||
@@ -1113,14 +1113,14 @@ class Subscription(Base):
|
|||||||
status = Column(String(20), default=SubscriptionStatus.TRIAL.value)
|
status = Column(String(20), default=SubscriptionStatus.TRIAL.value)
|
||||||
is_trial = Column(Boolean, default=True)
|
is_trial = Column(Boolean, default=True)
|
||||||
|
|
||||||
start_date = Column(DateTime, default=func.now())
|
start_date = Column(DateTime(timezone=True), default=func.now())
|
||||||
end_date = Column(DateTime, nullable=False)
|
end_date = Column(DateTime(timezone=True), nullable=False)
|
||||||
|
|
||||||
traffic_limit_gb = Column(Integer, default=0)
|
traffic_limit_gb = Column(Integer, default=0)
|
||||||
traffic_used_gb = Column(Float, default=0.0)
|
traffic_used_gb = Column(Float, default=0.0)
|
||||||
purchased_traffic_gb = Column(Integer, default=0) # Докупленный трафик
|
purchased_traffic_gb = Column(Integer, default=0) # Докупленный трафик
|
||||||
traffic_reset_at = Column(
|
traffic_reset_at = Column(
|
||||||
DateTime, nullable=True
|
DateTime(timezone=True), nullable=True
|
||||||
) # Дата сброса докупленного трафика (30 дней после первой докупки)
|
) # Дата сброса докупленного трафика (30 дней после первой докупки)
|
||||||
|
|
||||||
subscription_url = Column(String, nullable=True)
|
subscription_url = Column(String, nullable=True)
|
||||||
@@ -1134,10 +1134,10 @@ class Subscription(Base):
|
|||||||
autopay_enabled = Column(Boolean, default=False)
|
autopay_enabled = Column(Boolean, default=False)
|
||||||
autopay_days_before = Column(Integer, default=3)
|
autopay_days_before = Column(Integer, default=3)
|
||||||
|
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
last_webhook_update_at = Column(DateTime, nullable=True)
|
last_webhook_update_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
remnawave_short_uuid = Column(String(255), nullable=True)
|
remnawave_short_uuid = Column(String(255), nullable=True)
|
||||||
|
|
||||||
@@ -1148,7 +1148,7 @@ class Subscription(Base):
|
|||||||
is_daily_paused = Column(
|
is_daily_paused = Column(
|
||||||
Boolean, default=False, nullable=False
|
Boolean, default=False, nullable=False
|
||||||
) # Приостановлена ли суточная подписка пользователем
|
) # Приостановлена ли суточная подписка пользователем
|
||||||
last_daily_charge_at = Column(DateTime, nullable=True) # Время последнего суточного списания
|
last_daily_charge_at = Column(DateTime(timezone=True), nullable=True) # Время последнего суточного списания
|
||||||
|
|
||||||
user = relationship('User', back_populates='subscription')
|
user = relationship('User', back_populates='subscription')
|
||||||
tariff = relationship('Tariff', back_populates='subscriptions')
|
tariff = relationship('Tariff', back_populates='subscriptions')
|
||||||
@@ -1162,7 +1162,7 @@ class Subscription(Base):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_active(self) -> bool:
|
def is_active(self) -> bool:
|
||||||
current_time = datetime.utcnow()
|
current_time = datetime.now(UTC)
|
||||||
return (
|
return (
|
||||||
self.status == SubscriptionStatus.ACTIVE.value
|
self.status == SubscriptionStatus.ACTIVE.value
|
||||||
and self.end_date is not None
|
and self.end_date is not None
|
||||||
@@ -1172,11 +1172,11 @@ class Subscription(Base):
|
|||||||
@property
|
@property
|
||||||
def is_expired(self) -> bool:
|
def is_expired(self) -> bool:
|
||||||
"""Проверяет, истёк ли срок подписки"""
|
"""Проверяет, истёк ли срок подписки"""
|
||||||
return self.end_date is not None and self.end_date <= datetime.utcnow()
|
return self.end_date is not None and self.end_date <= datetime.now(UTC)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_be_expired(self) -> bool:
|
def should_be_expired(self) -> bool:
|
||||||
current_time = datetime.utcnow()
|
current_time = datetime.now(UTC)
|
||||||
return (
|
return (
|
||||||
self.status == SubscriptionStatus.ACTIVE.value
|
self.status == SubscriptionStatus.ACTIVE.value
|
||||||
and self.end_date is not None
|
and self.end_date is not None
|
||||||
@@ -1185,7 +1185,7 @@ class Subscription(Base):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def actual_status(self) -> str:
|
def actual_status(self) -> str:
|
||||||
current_time = datetime.utcnow()
|
current_time = datetime.now(UTC)
|
||||||
|
|
||||||
if self.status == SubscriptionStatus.EXPIRED.value:
|
if self.status == SubscriptionStatus.EXPIRED.value:
|
||||||
return 'expired'
|
return 'expired'
|
||||||
@@ -1208,7 +1208,6 @@ class Subscription(Base):
|
|||||||
@property
|
@property
|
||||||
def status_display(self) -> str:
|
def status_display(self) -> str:
|
||||||
actual_status = self.actual_status
|
actual_status = self.actual_status
|
||||||
datetime.utcnow()
|
|
||||||
|
|
||||||
if actual_status == 'expired':
|
if actual_status == 'expired':
|
||||||
return '🔴 Истекла'
|
return '🔴 Истекла'
|
||||||
@@ -1244,7 +1243,7 @@ class Subscription(Base):
|
|||||||
def days_left(self) -> int:
|
def days_left(self) -> int:
|
||||||
if self.end_date is None:
|
if self.end_date is None:
|
||||||
return 0
|
return 0
|
||||||
current_time = datetime.utcnow()
|
current_time = datetime.now(UTC)
|
||||||
if self.end_date <= current_time:
|
if self.end_date <= current_time:
|
||||||
return 0
|
return 0
|
||||||
delta = self.end_date - current_time
|
delta = self.end_date - current_time
|
||||||
@@ -1252,7 +1251,7 @@ class Subscription(Base):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def time_left_display(self) -> str:
|
def time_left_display(self) -> str:
|
||||||
current_time = datetime.utcnow()
|
current_time = datetime.now(UTC)
|
||||||
if self.end_date <= current_time:
|
if self.end_date <= current_time:
|
||||||
return 'истёк'
|
return 'истёк'
|
||||||
|
|
||||||
@@ -1275,10 +1274,10 @@ class Subscription(Base):
|
|||||||
return min((used / self.traffic_limit_gb) * 100, 100.0)
|
return min((used / self.traffic_limit_gb) * 100, 100.0)
|
||||||
|
|
||||||
def extend_subscription(self, days: int):
|
def extend_subscription(self, days: int):
|
||||||
if self.end_date > datetime.utcnow():
|
if self.end_date > datetime.now(UTC):
|
||||||
self.end_date = self.end_date + timedelta(days=days)
|
self.end_date = self.end_date + timedelta(days=days)
|
||||||
else:
|
else:
|
||||||
self.end_date = datetime.utcnow() + timedelta(days=days)
|
self.end_date = datetime.now(UTC) + timedelta(days=days)
|
||||||
|
|
||||||
if self.status == SubscriptionStatus.EXPIRED.value:
|
if self.status == SubscriptionStatus.EXPIRED.value:
|
||||||
self.status = SubscriptionStatus.ACTIVE.value
|
self.status = SubscriptionStatus.ACTIVE.value
|
||||||
@@ -1323,16 +1322,16 @@ class TrafficPurchase(Base):
|
|||||||
subscription_id = Column(Integer, ForeignKey('subscriptions.id', ondelete='CASCADE'), nullable=False, index=True)
|
subscription_id = Column(Integer, ForeignKey('subscriptions.id', ondelete='CASCADE'), nullable=False, index=True)
|
||||||
|
|
||||||
traffic_gb = Column(Integer, nullable=False) # Количество ГБ в покупке
|
traffic_gb = Column(Integer, nullable=False) # Количество ГБ в покупке
|
||||||
expires_at = Column(DateTime, nullable=False, index=True) # Дата истечения (покупка + 30 дней)
|
expires_at = Column(DateTime(timezone=True), nullable=False, index=True) # Дата истечения (покупка + 30 дней)
|
||||||
|
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
|
|
||||||
subscription = relationship('Subscription', back_populates='traffic_purchases')
|
subscription = relationship('Subscription', back_populates='traffic_purchases')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_expired(self) -> bool:
|
def is_expired(self) -> bool:
|
||||||
"""Проверяет, истекла ли докупка."""
|
"""Проверяет, истекла ли докупка."""
|
||||||
return datetime.utcnow() >= self.expires_at
|
return datetime.now(UTC) >= self.expires_at
|
||||||
|
|
||||||
|
|
||||||
class Transaction(Base):
|
class Transaction(Base):
|
||||||
@@ -1352,10 +1351,10 @@ class Transaction(Base):
|
|||||||
|
|
||||||
# NaloGO чек
|
# NaloGO чек
|
||||||
receipt_uuid = Column(String(255), nullable=True, index=True)
|
receipt_uuid = Column(String(255), nullable=True, index=True)
|
||||||
receipt_created_at = Column(DateTime, nullable=True)
|
receipt_created_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
completed_at = Column(DateTime, nullable=True)
|
completed_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
user = relationship('User', back_populates='transactions')
|
user = relationship('User', back_populates='transactions')
|
||||||
|
|
||||||
@@ -1370,7 +1369,7 @@ class SubscriptionConversion(Base):
|
|||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
|
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
|
||||||
|
|
||||||
converted_at = Column(DateTime, default=func.now())
|
converted_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
|
|
||||||
trial_duration_days = Column(Integer, nullable=True)
|
trial_duration_days = Column(Integer, nullable=True)
|
||||||
|
|
||||||
@@ -1380,7 +1379,7 @@ class SubscriptionConversion(Base):
|
|||||||
|
|
||||||
first_paid_period_days = Column(Integer, nullable=True)
|
first_paid_period_days = Column(Integer, nullable=True)
|
||||||
|
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
|
|
||||||
user = relationship('User', backref='subscription_conversions')
|
user = relationship('User', backref='subscription_conversions')
|
||||||
|
|
||||||
@@ -1406,8 +1405,8 @@ class PromoCode(Base):
|
|||||||
max_uses = Column(Integer, default=1)
|
max_uses = Column(Integer, default=1)
|
||||||
current_uses = Column(Integer, default=0)
|
current_uses = Column(Integer, default=0)
|
||||||
|
|
||||||
valid_from = Column(DateTime, default=func.now())
|
valid_from = Column(DateTime(timezone=True), default=func.now())
|
||||||
valid_until = Column(DateTime, nullable=True)
|
valid_until = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
is_active = Column(Boolean, default=True)
|
is_active = Column(Boolean, default=True)
|
||||||
first_purchase_only = Column(Boolean, default=False) # Только для первой покупки
|
first_purchase_only = Column(Boolean, default=False) # Только для первой покупки
|
||||||
@@ -1415,15 +1414,15 @@ class PromoCode(Base):
|
|||||||
created_by = Column(Integer, ForeignKey('users.id'), nullable=True)
|
created_by = Column(Integer, ForeignKey('users.id'), nullable=True)
|
||||||
promo_group_id = Column(Integer, ForeignKey('promo_groups.id', ondelete='SET NULL'), nullable=True, index=True)
|
promo_group_id = Column(Integer, ForeignKey('promo_groups.id', ondelete='SET NULL'), nullable=True, index=True)
|
||||||
|
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
uses = relationship('PromoCodeUse', back_populates='promocode')
|
uses = relationship('PromoCodeUse', back_populates='promocode')
|
||||||
promo_group = relationship('PromoGroup')
|
promo_group = relationship('PromoGroup')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_valid(self) -> bool:
|
def is_valid(self) -> bool:
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
return (
|
return (
|
||||||
self.is_active
|
self.is_active
|
||||||
and self.current_uses < self.max_uses
|
and self.current_uses < self.max_uses
|
||||||
@@ -1443,7 +1442,7 @@ class PromoCodeUse(Base):
|
|||||||
promocode_id = Column(Integer, ForeignKey('promocodes.id'), nullable=False)
|
promocode_id = Column(Integer, ForeignKey('promocodes.id'), nullable=False)
|
||||||
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
|
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
|
||||||
|
|
||||||
used_at = Column(DateTime, default=func.now())
|
used_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
|
|
||||||
promocode = relationship('PromoCode', back_populates='uses')
|
promocode = relationship('PromoCode', back_populates='uses')
|
||||||
user = relationship('User')
|
user = relationship('User')
|
||||||
@@ -1461,7 +1460,7 @@ class ReferralEarning(Base):
|
|||||||
|
|
||||||
referral_transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
|
referral_transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
|
||||||
|
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
|
|
||||||
user = relationship('User', foreign_keys=[user_id], back_populates='referral_earnings')
|
user = relationship('User', foreign_keys=[user_id], back_populates='referral_earnings')
|
||||||
referral = relationship('User', foreign_keys=[referral_id])
|
referral = relationship('User', foreign_keys=[referral_id])
|
||||||
@@ -1502,11 +1501,11 @@ class WithdrawalRequest(Base):
|
|||||||
|
|
||||||
# Обработка админом
|
# Обработка админом
|
||||||
processed_by = Column(Integer, ForeignKey('users.id'), nullable=True)
|
processed_by = Column(Integer, ForeignKey('users.id'), nullable=True)
|
||||||
processed_at = Column(DateTime, nullable=True)
|
processed_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
admin_comment = Column(Text, nullable=True)
|
admin_comment = Column(Text, nullable=True)
|
||||||
|
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
user = relationship('User', foreign_keys=[user_id], backref='withdrawal_requests')
|
user = relationship('User', foreign_keys=[user_id], backref='withdrawal_requests')
|
||||||
admin = relationship('User', foreign_keys=[processed_by])
|
admin = relationship('User', foreign_keys=[processed_by])
|
||||||
@@ -1524,18 +1523,18 @@ class ReferralContest(Base):
|
|||||||
description = Column(Text, nullable=True)
|
description = Column(Text, nullable=True)
|
||||||
prize_text = Column(Text, nullable=True)
|
prize_text = Column(Text, nullable=True)
|
||||||
contest_type = Column(String(50), nullable=False, default='referral_paid')
|
contest_type = Column(String(50), nullable=False, default='referral_paid')
|
||||||
start_at = Column(DateTime, nullable=False)
|
start_at = Column(DateTime(timezone=True), nullable=False)
|
||||||
end_at = Column(DateTime, nullable=False)
|
end_at = Column(DateTime(timezone=True), nullable=False)
|
||||||
daily_summary_time = Column(Time, nullable=False, default=time(hour=12, minute=0))
|
daily_summary_time = Column(Time, nullable=False, default=time(hour=12, minute=0))
|
||||||
daily_summary_times = Column(String(255), nullable=True) # CSV HH:MM
|
daily_summary_times = Column(String(255), nullable=True) # CSV HH:MM
|
||||||
timezone = Column(String(64), nullable=False, default='UTC')
|
timezone = Column(String(64), nullable=False, default='UTC')
|
||||||
is_active = Column(Boolean, nullable=False, default=True)
|
is_active = Column(Boolean, nullable=False, default=True)
|
||||||
last_daily_summary_date = Column(Date, nullable=True)
|
last_daily_summary_date = Column(Date, nullable=True)
|
||||||
last_daily_summary_at = Column(DateTime, nullable=True)
|
last_daily_summary_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
final_summary_sent = Column(Boolean, nullable=False, default=False)
|
final_summary_sent = Column(Boolean, nullable=False, default=False)
|
||||||
created_by = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True)
|
created_by = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True)
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
creator = relationship('User', backref='created_referral_contests')
|
creator = relationship('User', backref='created_referral_contests')
|
||||||
events = relationship(
|
events = relationship(
|
||||||
@@ -1565,7 +1564,7 @@ class ReferralContestEvent(Base):
|
|||||||
referral_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), nullable=False)
|
referral_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), nullable=False)
|
||||||
event_type = Column(String(50), nullable=False)
|
event_type = Column(String(50), nullable=False)
|
||||||
amount_kopeks = Column(Integer, nullable=False, default=0)
|
amount_kopeks = Column(Integer, nullable=False, default=0)
|
||||||
occurred_at = Column(DateTime, nullable=False, default=func.now())
|
occurred_at = Column(DateTime(timezone=True), nullable=False, default=func.now())
|
||||||
|
|
||||||
contest = relationship('ReferralContest', back_populates='events')
|
contest = relationship('ReferralContest', back_populates='events')
|
||||||
referrer = relationship('User', foreign_keys=[referrer_id])
|
referrer = relationship('User', foreign_keys=[referrer_id])
|
||||||
@@ -1585,7 +1584,7 @@ class ReferralContestVirtualParticipant(Base):
|
|||||||
display_name = Column(String(255), nullable=False)
|
display_name = Column(String(255), nullable=False)
|
||||||
referral_count = Column(Integer, nullable=False, default=0)
|
referral_count = Column(Integer, nullable=False, default=0)
|
||||||
total_amount_kopeks = Column(Integer, nullable=False, default=0)
|
total_amount_kopeks = Column(Integer, nullable=False, default=0)
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
|
|
||||||
contest = relationship('ReferralContest')
|
contest = relationship('ReferralContest')
|
||||||
|
|
||||||
@@ -1611,8 +1610,8 @@ class ContestTemplate(Base):
|
|||||||
cooldown_hours = Column(Integer, nullable=False, default=24)
|
cooldown_hours = Column(Integer, nullable=False, default=24)
|
||||||
payload = Column(JSON, nullable=True)
|
payload = Column(JSON, nullable=True)
|
||||||
is_enabled = Column(Boolean, nullable=False, default=True)
|
is_enabled = Column(Boolean, nullable=False, default=True)
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
rounds = relationship('ContestRound', back_populates='template')
|
rounds = relationship('ContestRound', back_populates='template')
|
||||||
|
|
||||||
@@ -1626,8 +1625,8 @@ class ContestRound(Base):
|
|||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
template_id = Column(Integer, ForeignKey('contest_templates.id', ondelete='CASCADE'), nullable=False)
|
template_id = Column(Integer, ForeignKey('contest_templates.id', ondelete='CASCADE'), nullable=False)
|
||||||
starts_at = Column(DateTime, nullable=False)
|
starts_at = Column(DateTime(timezone=True), nullable=False)
|
||||||
ends_at = Column(DateTime, nullable=False)
|
ends_at = Column(DateTime(timezone=True), nullable=False)
|
||||||
status = Column(String(20), nullable=False, default='active') # active, finished
|
status = Column(String(20), nullable=False, default='active') # active, finished
|
||||||
payload = Column(JSON, nullable=True)
|
payload = Column(JSON, nullable=True)
|
||||||
winners_count = Column(Integer, nullable=False, default=0)
|
winners_count = Column(Integer, nullable=False, default=0)
|
||||||
@@ -1635,8 +1634,8 @@ class ContestRound(Base):
|
|||||||
attempts_per_user = Column(Integer, nullable=False, default=1)
|
attempts_per_user = Column(Integer, nullable=False, default=1)
|
||||||
message_id = Column(BigInteger, nullable=True)
|
message_id = Column(BigInteger, nullable=True)
|
||||||
chat_id = Column(BigInteger, nullable=True)
|
chat_id = Column(BigInteger, nullable=True)
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
template = relationship('ContestTemplate', back_populates='rounds')
|
template = relationship('ContestTemplate', back_populates='rounds')
|
||||||
attempts = relationship('ContestAttempt', back_populates='round', cascade='all, delete-orphan')
|
attempts = relationship('ContestAttempt', back_populates='round', cascade='all, delete-orphan')
|
||||||
@@ -1654,7 +1653,7 @@ class ContestAttempt(Base):
|
|||||||
user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), nullable=False)
|
user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), nullable=False)
|
||||||
answer = Column(Text, nullable=True)
|
answer = Column(Text, nullable=True)
|
||||||
is_winner = Column(Boolean, nullable=False, default=False)
|
is_winner = Column(Boolean, nullable=False, default=False)
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
|
|
||||||
round = relationship('ContestRound', back_populates='attempts')
|
round = relationship('ContestRound', back_populates='attempts')
|
||||||
user = relationship('User')
|
user = relationship('User')
|
||||||
@@ -1674,8 +1673,8 @@ class Squad(Base):
|
|||||||
|
|
||||||
description = Column(Text, nullable=True)
|
description = Column(Text, nullable=True)
|
||||||
|
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def price_rubles(self) -> float:
|
def price_rubles(self) -> float:
|
||||||
@@ -1696,8 +1695,8 @@ class ServiceRule(Base):
|
|||||||
|
|
||||||
language = Column(String(5), default='ru')
|
language = Column(String(5), default='ru')
|
||||||
|
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
|
||||||
class PrivacyPolicy(Base):
|
class PrivacyPolicy(Base):
|
||||||
@@ -1707,8 +1706,8 @@ class PrivacyPolicy(Base):
|
|||||||
language = Column(String(10), nullable=False, unique=True)
|
language = Column(String(10), nullable=False, unique=True)
|
||||||
content = Column(Text, nullable=False)
|
content = Column(Text, nullable=False)
|
||||||
is_enabled = Column(Boolean, default=True, nullable=False)
|
is_enabled = Column(Boolean, default=True, nullable=False)
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
|
||||||
class PublicOffer(Base):
|
class PublicOffer(Base):
|
||||||
@@ -1718,8 +1717,8 @@ class PublicOffer(Base):
|
|||||||
language = Column(String(10), nullable=False, unique=True)
|
language = Column(String(10), nullable=False, unique=True)
|
||||||
content = Column(Text, nullable=False)
|
content = Column(Text, nullable=False)
|
||||||
is_enabled = Column(Boolean, default=True, nullable=False)
|
is_enabled = Column(Boolean, default=True, nullable=False)
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
|
||||||
class FaqSetting(Base):
|
class FaqSetting(Base):
|
||||||
@@ -1728,8 +1727,8 @@ class FaqSetting(Base):
|
|||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
language = Column(String(10), nullable=False, unique=True)
|
language = Column(String(10), nullable=False, unique=True)
|
||||||
is_enabled = Column(Boolean, default=True, nullable=False)
|
is_enabled = Column(Boolean, default=True, nullable=False)
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
|
||||||
class FaqPage(Base):
|
class FaqPage(Base):
|
||||||
@@ -1741,8 +1740,8 @@ class FaqPage(Base):
|
|||||||
content = Column(Text, nullable=False)
|
content = Column(Text, nullable=False)
|
||||||
display_order = Column(Integer, default=0, nullable=False)
|
display_order = Column(Integer, default=0, nullable=False)
|
||||||
is_active = Column(Boolean, default=True, nullable=False)
|
is_active = Column(Boolean, default=True, nullable=False)
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
|
||||||
class SystemSetting(Base):
|
class SystemSetting(Base):
|
||||||
@@ -1753,8 +1752,8 @@ class SystemSetting(Base):
|
|||||||
value = Column(Text, nullable=True)
|
value = Column(Text, nullable=True)
|
||||||
description = Column(Text, nullable=True)
|
description = Column(Text, nullable=True)
|
||||||
|
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
|
||||||
class MonitoringLog(Base):
|
class MonitoringLog(Base):
|
||||||
@@ -1769,7 +1768,7 @@ class MonitoringLog(Base):
|
|||||||
|
|
||||||
is_success = Column(Boolean, default=True)
|
is_success = Column(Boolean, default=True)
|
||||||
|
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
|
|
||||||
|
|
||||||
class SentNotification(Base):
|
class SentNotification(Base):
|
||||||
@@ -1780,7 +1779,7 @@ class SentNotification(Base):
|
|||||||
subscription_id = Column(Integer, ForeignKey('subscriptions.id', ondelete='CASCADE'), nullable=False)
|
subscription_id = Column(Integer, ForeignKey('subscriptions.id', ondelete='CASCADE'), nullable=False)
|
||||||
notification_type = Column(String(50), nullable=False)
|
notification_type = Column(String(50), nullable=False)
|
||||||
days_before = Column(Integer, nullable=True)
|
days_before = Column(Integer, nullable=True)
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
|
|
||||||
user = relationship('User', backref='sent_notifications')
|
user = relationship('User', backref='sent_notifications')
|
||||||
subscription = relationship('Subscription', backref=backref('sent_notifications', passive_deletes=True))
|
subscription = relationship('Subscription', backref=backref('sent_notifications', passive_deletes=True))
|
||||||
@@ -1797,9 +1796,9 @@ class SubscriptionEvent(Base):
|
|||||||
amount_kopeks = Column(Integer, nullable=True)
|
amount_kopeks = Column(Integer, nullable=True)
|
||||||
currency = Column(String(16), nullable=True)
|
currency = Column(String(16), nullable=True)
|
||||||
message = Column(Text, nullable=True)
|
message = Column(Text, nullable=True)
|
||||||
occurred_at = Column(DateTime, nullable=False, default=func.now())
|
occurred_at = Column(DateTime(timezone=True), nullable=False, default=func.now())
|
||||||
extra = Column(JSON, nullable=True)
|
extra = Column(JSON, nullable=True)
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
|
|
||||||
user = relationship('User', backref='subscription_events')
|
user = relationship('User', backref='subscription_events')
|
||||||
subscription = relationship('Subscription', backref='subscription_events')
|
subscription = relationship('Subscription', backref='subscription_events')
|
||||||
@@ -1816,13 +1815,13 @@ class DiscountOffer(Base):
|
|||||||
notification_type = Column(String(50), nullable=False)
|
notification_type = Column(String(50), nullable=False)
|
||||||
discount_percent = Column(Integer, nullable=False, default=0)
|
discount_percent = Column(Integer, nullable=False, default=0)
|
||||||
bonus_amount_kopeks = Column(Integer, nullable=False, default=0)
|
bonus_amount_kopeks = Column(Integer, nullable=False, default=0)
|
||||||
expires_at = Column(DateTime, nullable=False)
|
expires_at = Column(DateTime(timezone=True), nullable=False)
|
||||||
claimed_at = Column(DateTime, nullable=True)
|
claimed_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
is_active = Column(Boolean, default=True, nullable=False)
|
is_active = Column(Boolean, default=True, nullable=False)
|
||||||
effect_type = Column(String(50), nullable=False, default='percent_discount')
|
effect_type = Column(String(50), nullable=False, default='percent_discount')
|
||||||
extra_data = Column(JSON, nullable=True)
|
extra_data = Column(JSON, nullable=True)
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
user = relationship('User', back_populates='discount_offers')
|
user = relationship('User', back_populates='discount_offers')
|
||||||
subscription = relationship('Subscription', back_populates='discount_offers')
|
subscription = relationship('Subscription', back_populates='discount_offers')
|
||||||
@@ -1846,8 +1845,8 @@ class PromoOfferTemplate(Base):
|
|||||||
test_squad_uuids = Column(JSON, default=list)
|
test_squad_uuids = Column(JSON, default=list)
|
||||||
is_active = Column(Boolean, default=True, nullable=False)
|
is_active = Column(Boolean, default=True, nullable=False)
|
||||||
created_by = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True)
|
created_by = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True)
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
creator = relationship('User')
|
creator = relationship('User')
|
||||||
|
|
||||||
@@ -1859,9 +1858,9 @@ class SubscriptionTemporaryAccess(Base):
|
|||||||
subscription_id = Column(Integer, ForeignKey('subscriptions.id', ondelete='CASCADE'), nullable=False)
|
subscription_id = Column(Integer, ForeignKey('subscriptions.id', ondelete='CASCADE'), nullable=False)
|
||||||
offer_id = Column(Integer, ForeignKey('discount_offers.id', ondelete='CASCADE'), nullable=False)
|
offer_id = Column(Integer, ForeignKey('discount_offers.id', ondelete='CASCADE'), nullable=False)
|
||||||
squad_uuid = Column(String(255), nullable=False)
|
squad_uuid = Column(String(255), nullable=False)
|
||||||
expires_at = Column(DateTime, nullable=False)
|
expires_at = Column(DateTime(timezone=True), nullable=False)
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
deactivated_at = Column(DateTime, nullable=True)
|
deactivated_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
is_active = Column(Boolean, default=True, nullable=False)
|
is_active = Column(Boolean, default=True, nullable=False)
|
||||||
was_already_connected = Column(Boolean, default=False, nullable=False)
|
was_already_connected = Column(Boolean, default=False, nullable=False)
|
||||||
|
|
||||||
@@ -1880,7 +1879,7 @@ class PromoOfferLog(Base):
|
|||||||
percent = Column(Integer, nullable=True)
|
percent = Column(Integer, nullable=True)
|
||||||
effect_type = Column(String(50), nullable=True)
|
effect_type = Column(String(50), nullable=True)
|
||||||
details = Column(JSON, nullable=True)
|
details = Column(JSON, nullable=True)
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
|
|
||||||
user = relationship('User', back_populates='promo_offer_logs')
|
user = relationship('User', back_populates='promo_offer_logs')
|
||||||
offer = relationship('DiscountOffer', back_populates='logs')
|
offer = relationship('DiscountOffer', back_populates='logs')
|
||||||
@@ -1922,8 +1921,8 @@ class Poll(Base):
|
|||||||
reward_enabled = Column(Boolean, nullable=False, default=False)
|
reward_enabled = Column(Boolean, nullable=False, default=False)
|
||||||
reward_amount_kopeks = Column(Integer, nullable=False, default=0)
|
reward_amount_kopeks = Column(Integer, nullable=False, default=0)
|
||||||
created_by = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True)
|
created_by = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True)
|
||||||
created_at = Column(DateTime, default=func.now(), nullable=False)
|
created_at = Column(DateTime(timezone=True), default=func.now(), nullable=False)
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now(), nullable=False)
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now(), nullable=False)
|
||||||
|
|
||||||
creator = relationship('User', backref='created_polls', foreign_keys=[created_by])
|
creator = relationship('User', backref='created_polls', foreign_keys=[created_by])
|
||||||
questions = relationship(
|
questions = relationship(
|
||||||
@@ -1975,9 +1974,9 @@ class PollResponse(Base):
|
|||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
poll_id = Column(Integer, ForeignKey('polls.id', ondelete='CASCADE'), nullable=False, index=True)
|
poll_id = Column(Integer, ForeignKey('polls.id', ondelete='CASCADE'), nullable=False, index=True)
|
||||||
user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), nullable=False, index=True)
|
user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), nullable=False, index=True)
|
||||||
sent_at = Column(DateTime, default=func.now(), nullable=False)
|
sent_at = Column(DateTime(timezone=True), default=func.now(), nullable=False)
|
||||||
started_at = Column(DateTime, nullable=True)
|
started_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
completed_at = Column(DateTime, nullable=True)
|
completed_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
reward_given = Column(Boolean, nullable=False, default=False)
|
reward_given = Column(Boolean, nullable=False, default=False)
|
||||||
reward_amount_kopeks = Column(Integer, nullable=False, default=0)
|
reward_amount_kopeks = Column(Integer, nullable=False, default=0)
|
||||||
|
|
||||||
@@ -1999,7 +1998,7 @@ class PollAnswer(Base):
|
|||||||
response_id = Column(Integer, ForeignKey('poll_responses.id', ondelete='CASCADE'), nullable=False, index=True)
|
response_id = Column(Integer, ForeignKey('poll_responses.id', ondelete='CASCADE'), nullable=False, index=True)
|
||||||
question_id = Column(Integer, ForeignKey('poll_questions.id', ondelete='CASCADE'), nullable=False, index=True)
|
question_id = Column(Integer, ForeignKey('poll_questions.id', ondelete='CASCADE'), nullable=False, index=True)
|
||||||
option_id = Column(Integer, ForeignKey('poll_options.id', ondelete='CASCADE'), nullable=False, index=True)
|
option_id = Column(Integer, ForeignKey('poll_options.id', ondelete='CASCADE'), nullable=False, index=True)
|
||||||
created_at = Column(DateTime, default=func.now(), nullable=False)
|
created_at = Column(DateTime(timezone=True), default=func.now(), nullable=False)
|
||||||
|
|
||||||
response = relationship('PollResponse', back_populates='answers')
|
response = relationship('PollResponse', back_populates='answers')
|
||||||
question = relationship('PollQuestion', back_populates='answers')
|
question = relationship('PollQuestion', back_populates='answers')
|
||||||
@@ -2033,8 +2032,8 @@ class ServerSquad(Base):
|
|||||||
max_users = Column(Integer, nullable=True)
|
max_users = Column(Integer, nullable=True)
|
||||||
current_users = Column(Integer, default=0)
|
current_users = Column(Integer, default=0)
|
||||||
|
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
allowed_promo_groups = relationship(
|
allowed_promo_groups = relationship(
|
||||||
'PromoGroup',
|
'PromoGroup',
|
||||||
@@ -2069,7 +2068,7 @@ class SubscriptionServer(Base):
|
|||||||
subscription_id = Column(Integer, ForeignKey('subscriptions.id'), nullable=False)
|
subscription_id = Column(Integer, ForeignKey('subscriptions.id'), nullable=False)
|
||||||
server_squad_id = Column(Integer, ForeignKey('server_squads.id'), nullable=False)
|
server_squad_id = Column(Integer, ForeignKey('server_squads.id'), nullable=False)
|
||||||
|
|
||||||
connected_at = Column(DateTime, default=func.now())
|
connected_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
|
|
||||||
paid_price_kopeks = Column(Integer, default=0)
|
paid_price_kopeks = Column(Integer, default=0)
|
||||||
|
|
||||||
@@ -2088,7 +2087,7 @@ class SupportAuditLog(Base):
|
|||||||
ticket_id = Column(Integer, ForeignKey('tickets.id', ondelete='SET NULL'), nullable=True)
|
ticket_id = Column(Integer, ForeignKey('tickets.id', ondelete='SET NULL'), nullable=True)
|
||||||
target_user_id = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True)
|
target_user_id = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True)
|
||||||
details = Column(JSON, nullable=True)
|
details = Column(JSON, nullable=True)
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
|
|
||||||
actor = relationship('User', foreign_keys=[actor_user_id])
|
actor = relationship('User', foreign_keys=[actor_user_id])
|
||||||
ticket = relationship('Ticket', foreign_keys=[ticket_id])
|
ticket = relationship('Ticket', foreign_keys=[ticket_id])
|
||||||
@@ -2101,8 +2100,8 @@ class UserMessage(Base):
|
|||||||
is_active = Column(Boolean, default=True)
|
is_active = Column(Boolean, default=True)
|
||||||
sort_order = Column(Integer, default=0)
|
sort_order = Column(Integer, default=0)
|
||||||
created_by = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True)
|
created_by = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True)
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
creator = relationship('User', backref='created_messages')
|
creator = relationship('User', backref='created_messages')
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
@@ -2117,8 +2116,8 @@ class WelcomeText(Base):
|
|||||||
is_active = Column(Boolean, default=True)
|
is_active = Column(Boolean, default=True)
|
||||||
is_enabled = Column(Boolean, default=True)
|
is_enabled = Column(Boolean, default=True)
|
||||||
created_by = Column(Integer, ForeignKey('users.id'), nullable=True)
|
created_by = Column(Integer, ForeignKey('users.id'), nullable=True)
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
creator = relationship('User', backref='created_welcome_texts')
|
creator = relationship('User', backref='created_welcome_texts')
|
||||||
|
|
||||||
@@ -2134,8 +2133,8 @@ class PinnedMessage(Base):
|
|||||||
send_on_every_start = Column(Boolean, nullable=False, server_default='1', default=True)
|
send_on_every_start = Column(Boolean, nullable=False, server_default='1', default=True)
|
||||||
is_active = Column(Boolean, default=True)
|
is_active = Column(Boolean, default=True)
|
||||||
created_by = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True)
|
created_by = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True)
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
creator = relationship('User', backref='pinned_messages')
|
creator = relationship('User', backref='pinned_messages')
|
||||||
|
|
||||||
@@ -2162,8 +2161,8 @@ class AdvertisingCampaign(Base):
|
|||||||
is_active = Column(Boolean, default=True)
|
is_active = Column(Boolean, default=True)
|
||||||
|
|
||||||
created_by = Column(Integer, ForeignKey('users.id'), nullable=True)
|
created_by = Column(Integer, ForeignKey('users.id'), nullable=True)
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
registrations = relationship('AdvertisingCampaignRegistration', back_populates='campaign')
|
registrations = relationship('AdvertisingCampaignRegistration', back_populates='campaign')
|
||||||
tariff = relationship('Tariff', foreign_keys=[tariff_id])
|
tariff = relationship('Tariff', foreign_keys=[tariff_id])
|
||||||
@@ -2203,7 +2202,7 @@ class AdvertisingCampaignRegistration(Base):
|
|||||||
tariff_id = Column(Integer, ForeignKey('tariffs.id', ondelete='SET NULL'), nullable=True)
|
tariff_id = Column(Integer, ForeignKey('tariffs.id', ondelete='SET NULL'), nullable=True)
|
||||||
tariff_duration_days = Column(Integer, nullable=True)
|
tariff_duration_days = Column(Integer, nullable=True)
|
||||||
|
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
|
|
||||||
campaign = relationship('AdvertisingCampaign', back_populates='registrations')
|
campaign = relationship('AdvertisingCampaign', back_populates='registrations')
|
||||||
user = relationship('User')
|
user = relationship('User')
|
||||||
@@ -2232,13 +2231,13 @@ class Ticket(Base):
|
|||||||
priority = Column(String(20), default='normal', nullable=False) # low, normal, high, urgent
|
priority = Column(String(20), default='normal', nullable=False) # low, normal, high, urgent
|
||||||
# Блокировка ответов пользователя в этом тикете
|
# Блокировка ответов пользователя в этом тикете
|
||||||
user_reply_block_permanent = Column(Boolean, default=False, nullable=False)
|
user_reply_block_permanent = Column(Boolean, default=False, nullable=False)
|
||||||
user_reply_block_until = Column(DateTime, nullable=True)
|
user_reply_block_until = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
closed_at = Column(DateTime, nullable=True)
|
closed_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
# SLA reminders
|
# SLA reminders
|
||||||
last_sla_reminder_at = Column(DateTime, nullable=True)
|
last_sla_reminder_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
# Связи
|
# Связи
|
||||||
user = relationship('User', backref='tickets')
|
user = relationship('User', backref='tickets')
|
||||||
@@ -2266,9 +2265,9 @@ class Ticket(Base):
|
|||||||
return True
|
return True
|
||||||
if self.user_reply_block_until:
|
if self.user_reply_block_until:
|
||||||
try:
|
try:
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
return self.user_reply_block_until > datetime.utcnow()
|
return self.user_reply_block_until > datetime.now(UTC)
|
||||||
except Exception:
|
except Exception:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
@@ -2308,7 +2307,7 @@ class TicketMessage(Base):
|
|||||||
media_file_id = Column(String(255), nullable=True)
|
media_file_id = Column(String(255), nullable=True)
|
||||||
media_caption = Column(Text, nullable=True)
|
media_caption = Column(Text, nullable=True)
|
||||||
|
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
|
|
||||||
# Связи
|
# Связи
|
||||||
ticket = relationship('Ticket', back_populates='messages')
|
ticket = relationship('Ticket', back_populates='messages')
|
||||||
@@ -2334,10 +2333,10 @@ class WebApiToken(Base):
|
|||||||
token_hash = Column(String(128), nullable=False, unique=True, index=True)
|
token_hash = Column(String(128), nullable=False, unique=True, index=True)
|
||||||
token_prefix = Column(String(32), nullable=False, index=True)
|
token_prefix = Column(String(32), nullable=False, index=True)
|
||||||
description = Column(Text, nullable=True)
|
description = Column(Text, nullable=True)
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
expires_at = Column(DateTime, nullable=True)
|
expires_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
last_used_at = Column(DateTime, nullable=True)
|
last_used_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
last_used_ip = Column(String(64), nullable=True)
|
last_used_ip = Column(String(64), nullable=True)
|
||||||
is_active = Column(Boolean, default=True, nullable=False)
|
is_active = Column(Boolean, default=True, nullable=False)
|
||||||
created_by = Column(String(255), nullable=True)
|
created_by = Column(String(255), nullable=True)
|
||||||
@@ -2357,8 +2356,8 @@ class MainMenuButton(Base):
|
|||||||
visibility = Column(String(20), nullable=False, default=MainMenuButtonVisibility.ALL.value)
|
visibility = Column(String(20), nullable=False, default=MainMenuButtonVisibility.ALL.value)
|
||||||
is_active = Column(Boolean, nullable=False, default=True)
|
is_active = Column(Boolean, nullable=False, default=True)
|
||||||
display_order = Column(Integer, nullable=False, default=0)
|
display_order = Column(Integer, nullable=False, default=0)
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
__table_args__ = (Index('ix_main_menu_buttons_order', 'display_order', 'id'),)
|
__table_args__ = (Index('ix_main_menu_buttons_order', 'display_order', 'id'),)
|
||||||
|
|
||||||
@@ -2393,7 +2392,7 @@ class MenuLayoutHistory(Base):
|
|||||||
action = Column(String(50), nullable=False) # update, reset, import
|
action = Column(String(50), nullable=False) # update, reset, import
|
||||||
changes_summary = Column(Text, nullable=True) # Краткое описание изменений
|
changes_summary = Column(Text, nullable=True) # Краткое описание изменений
|
||||||
user_info = Column(String(255), nullable=True) # Информация о пользователе/токене
|
user_info = Column(String(255), nullable=True) # Информация о пользователе/токене
|
||||||
created_at = Column(DateTime, default=func.now(), index=True)
|
created_at = Column(DateTime(timezone=True), default=func.now(), index=True)
|
||||||
|
|
||||||
__table_args__ = (Index('ix_menu_layout_history_created', 'created_at'),)
|
__table_args__ = (Index('ix_menu_layout_history_created', 'created_at'),)
|
||||||
|
|
||||||
@@ -2410,7 +2409,7 @@ class ButtonClickLog(Base):
|
|||||||
button_id = Column(String(100), nullable=False, index=True) # ID кнопки
|
button_id = Column(String(100), nullable=False, index=True) # ID кнопки
|
||||||
user_id = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True, index=True)
|
user_id = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True, index=True)
|
||||||
callback_data = Column(String(255), nullable=True) # callback_data кнопки
|
callback_data = Column(String(255), nullable=True) # callback_data кнопки
|
||||||
clicked_at = Column(DateTime, default=func.now(), index=True)
|
clicked_at = Column(DateTime(timezone=True), default=func.now(), index=True)
|
||||||
|
|
||||||
# Дополнительная информация
|
# Дополнительная информация
|
||||||
button_type = Column(String(20), nullable=True, index=True) # builtin, callback, url, mini_app
|
button_type = Column(String(20), nullable=True, index=True) # builtin, callback, url, mini_app
|
||||||
@@ -2444,9 +2443,9 @@ class Webhook(Base):
|
|||||||
event_type = Column(String(50), nullable=False) # user.created, payment.completed, ticket.created, etc.
|
event_type = Column(String(50), nullable=False) # user.created, payment.completed, ticket.created, etc.
|
||||||
is_active = Column(Boolean, default=True, nullable=False)
|
is_active = Column(Boolean, default=True, nullable=False)
|
||||||
description = Column(Text, nullable=True)
|
description = Column(Text, nullable=True)
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
last_triggered_at = Column(DateTime, nullable=True)
|
last_triggered_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
failure_count = Column(Integer, default=0, nullable=False)
|
failure_count = Column(Integer, default=0, nullable=False)
|
||||||
success_count = Column(Integer, default=0, nullable=False)
|
success_count = Column(Integer, default=0, nullable=False)
|
||||||
|
|
||||||
@@ -2475,9 +2474,9 @@ class WebhookDelivery(Base):
|
|||||||
status = Column(String(20), nullable=False) # pending, success, failed
|
status = Column(String(20), nullable=False) # pending, success, failed
|
||||||
error_message = Column(Text, nullable=True)
|
error_message = Column(Text, nullable=True)
|
||||||
attempt_number = Column(Integer, default=1, nullable=False)
|
attempt_number = Column(Integer, default=1, nullable=False)
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
delivered_at = Column(DateTime, nullable=True)
|
delivered_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
next_retry_at = Column(DateTime, nullable=True)
|
next_retry_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
webhook = relationship('Webhook', back_populates='deliveries')
|
webhook = relationship('Webhook', back_populates='deliveries')
|
||||||
|
|
||||||
@@ -2495,15 +2494,15 @@ class CabinetRefreshToken(Base):
|
|||||||
user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), nullable=False)
|
user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), nullable=False)
|
||||||
token_hash = Column(String(255), unique=True, nullable=False, index=True)
|
token_hash = Column(String(255), unique=True, nullable=False, index=True)
|
||||||
device_info = Column(String(500), nullable=True)
|
device_info = Column(String(500), nullable=True)
|
||||||
expires_at = Column(DateTime, nullable=False)
|
expires_at = Column(DateTime(timezone=True), nullable=False)
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
revoked_at = Column(DateTime, nullable=True)
|
revoked_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
user = relationship('User', backref='cabinet_tokens')
|
user = relationship('User', backref='cabinet_tokens')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_expired(self) -> bool:
|
def is_expired(self) -> bool:
|
||||||
return datetime.utcnow() > self.expires_at
|
return datetime.now(UTC) > self.expires_at
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_revoked(self) -> bool:
|
def is_revoked(self) -> bool:
|
||||||
@@ -2549,8 +2548,8 @@ class WheelConfig(Base):
|
|||||||
promo_prefix = Column(String(20), default='WHEEL', nullable=False)
|
promo_prefix = Column(String(20), default='WHEEL', nullable=False)
|
||||||
promo_validity_days = Column(Integer, default=7, nullable=False)
|
promo_validity_days = Column(Integer, default=7, nullable=False)
|
||||||
|
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
prizes = relationship('WheelPrize', back_populates='config', cascade='all, delete-orphan')
|
prizes = relationship('WheelPrize', back_populates='config', cascade='all, delete-orphan')
|
||||||
|
|
||||||
@@ -2588,8 +2587,8 @@ class WheelPrize(Base):
|
|||||||
promo_subscription_days = Column(Integer, default=0)
|
promo_subscription_days = Column(Integer, default=0)
|
||||||
promo_traffic_gb = Column(Integer, default=0)
|
promo_traffic_gb = Column(Integer, default=0)
|
||||||
|
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
config = relationship('WheelConfig', back_populates='prizes')
|
config = relationship('WheelConfig', back_populates='prizes')
|
||||||
spins = relationship('WheelSpin', back_populates='prize')
|
spins = relationship('WheelSpin', back_populates='prize')
|
||||||
@@ -2624,9 +2623,9 @@ class WheelSpin(Base):
|
|||||||
|
|
||||||
# Флаг успешного начисления
|
# Флаг успешного начисления
|
||||||
is_applied = Column(Boolean, default=False, nullable=False)
|
is_applied = Column(Boolean, default=False, nullable=False)
|
||||||
applied_at = Column(DateTime, nullable=True)
|
applied_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
|
|
||||||
user = relationship('User', backref='wheel_spins')
|
user = relationship('User', backref='wheel_spins')
|
||||||
prize = relationship('WheelPrize', back_populates='spins')
|
prize = relationship('WheelPrize', back_populates='spins')
|
||||||
@@ -2671,8 +2670,8 @@ class TicketNotification(Base):
|
|||||||
# Прочитано ли уведомление
|
# Прочитано ли уведомление
|
||||||
is_read = Column(Boolean, default=False, nullable=False)
|
is_read = Column(Boolean, default=False, nullable=False)
|
||||||
|
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
read_at = Column(DateTime, nullable=True)
|
read_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
ticket = relationship('Ticket', backref='notifications')
|
ticket = relationship('Ticket', backref='notifications')
|
||||||
user = relationship('User', backref='ticket_notifications')
|
user = relationship('User', backref='ticket_notifications')
|
||||||
@@ -2729,8 +2728,8 @@ class PaymentMethodConfig(Base):
|
|||||||
lazy='selectin',
|
lazy='selectin',
|
||||||
)
|
)
|
||||||
|
|
||||||
created_at = Column(DateTime, default=func.now())
|
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<PaymentMethodConfig method_id='{self.method_id}' order={self.sort_order} enabled={self.is_enabled}>"
|
return f"<PaymentMethodConfig method_id='{self.method_id}' order={self.sort_order} enabled={self.is_enabled}>"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from sqlalchemy import select, text
|
from sqlalchemy import select, text
|
||||||
@@ -4720,7 +4720,7 @@ async def ensure_default_web_api_token() -> bool:
|
|||||||
updated = True
|
updated = True
|
||||||
|
|
||||||
if updated:
|
if updated:
|
||||||
existing.updated_at = datetime.utcnow()
|
existing.updated_at = datetime.now(UTC)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -6454,6 +6454,63 @@ async def migrate_cloudpayments_transaction_id_to_bigint() -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def migrate_datetime_to_timestamptz() -> bool:
|
||||||
|
"""Migrate all TIMESTAMP WITHOUT TIME ZONE columns to TIMESTAMP WITH TIME ZONE.
|
||||||
|
|
||||||
|
PostgreSQL treats existing naive values as UTC (session timezone) during conversion,
|
||||||
|
so existing data is preserved correctly.
|
||||||
|
SQLite does not have a separate timestamptz type, so this is a no-op.
|
||||||
|
"""
|
||||||
|
db_type = await get_database_type()
|
||||||
|
|
||||||
|
if db_type == 'sqlite':
|
||||||
|
logger.info('ℹ️ SQLite не требует миграции TIMESTAMPTZ')
|
||||||
|
return True
|
||||||
|
|
||||||
|
if db_type != 'postgresql':
|
||||||
|
logger.info('ℹ️ Миграция TIMESTAMPTZ поддерживается только для PostgreSQL', db_type=db_type)
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
result = await conn.execute(
|
||||||
|
text("""
|
||||||
|
SELECT table_name, column_name
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND data_type = 'timestamp without time zone'
|
||||||
|
ORDER BY table_name, column_name
|
||||||
|
""")
|
||||||
|
)
|
||||||
|
columns = result.fetchall()
|
||||||
|
|
||||||
|
if not columns:
|
||||||
|
logger.info('ℹ️ Все DateTime колонки уже TIMESTAMPTZ')
|
||||||
|
return True
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
'🔄 Найдено колонок для миграции на TIMESTAMPTZ',
|
||||||
|
count=len(columns),
|
||||||
|
)
|
||||||
|
|
||||||
|
await conn.execute(text("SET LOCAL timezone = 'UTC'"))
|
||||||
|
|
||||||
|
for table_name, column_name in columns:
|
||||||
|
await conn.execute(
|
||||||
|
text(f'ALTER TABLE "{table_name}" ALTER COLUMN "{column_name}" TYPE TIMESTAMP WITH TIME ZONE')
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
'✅ Мигрировано колонок на TIMESTAMPTZ',
|
||||||
|
count=len(columns),
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as error:
|
||||||
|
logger.error('❌ Ошибка миграции DateTime колонок на TIMESTAMPTZ', error=error)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def run_universal_migration():
|
async def run_universal_migration():
|
||||||
logger.info('=== НАЧАЛО УНИВЕРСАЛЬНОЙ МИГРАЦИИ ===')
|
logger.info('=== НАЧАЛО УНИВЕРСАЛЬНОЙ МИГРАЦИИ ===')
|
||||||
|
|
||||||
@@ -7130,6 +7187,13 @@ async def run_universal_migration():
|
|||||||
else:
|
else:
|
||||||
logger.warning('⚠️ Проблемы с колонкой last_webhook_update_at')
|
logger.warning('⚠️ Проблемы с колонкой last_webhook_update_at')
|
||||||
|
|
||||||
|
logger.info('=== МИГРАЦИЯ DATETIME КОЛОНОК НА TIMESTAMPTZ ===')
|
||||||
|
timestamptz_ready = await migrate_datetime_to_timestamptz()
|
||||||
|
if timestamptz_ready:
|
||||||
|
logger.info('✅ Все DateTime колонки мигрированы на TIMESTAMPTZ')
|
||||||
|
else:
|
||||||
|
logger.warning('⚠️ Проблемы с миграцией DateTime колонок')
|
||||||
|
|
||||||
async with engine.begin() as conn:
|
async with engine.begin() as conn:
|
||||||
total_subs = await conn.execute(text('SELECT COUNT(*) FROM subscriptions'))
|
total_subs = await conn.execute(text('SELECT COUNT(*) FROM subscriptions'))
|
||||||
unique_users = await conn.execute(text('SELECT COUNT(DISTINCT user_id) FROM subscriptions'))
|
unique_users = await conn.execute(text('SELECT COUNT(DISTINCT user_id) FROM subscriptions'))
|
||||||
|
|||||||
Vendored
+4
-4
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
from datetime import datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
@@ -33,12 +33,12 @@ class WataPublicKeyProvider:
|
|||||||
async def get_public_key(self) -> str | None:
|
async def get_public_key(self) -> str | None:
|
||||||
"""Returns a cached public key or fetches a new one from WATA."""
|
"""Returns a cached public key or fetches a new one from WATA."""
|
||||||
|
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
if self._cached_key and self._expires_at and now < self._expires_at:
|
if self._cached_key and self._expires_at and now < self._expires_at:
|
||||||
return self._cached_key
|
return self._cached_key
|
||||||
|
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
if self._cached_key and self._expires_at and now < self._expires_at:
|
if self._cached_key and self._expires_at and now < self._expires_at:
|
||||||
return self._cached_key
|
return self._cached_key
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ class WataPublicKeyProvider:
|
|||||||
if key:
|
if key:
|
||||||
self._cached_key = key
|
self._cached_key = key
|
||||||
if self._cache_seconds > 0:
|
if self._cache_seconds > 0:
|
||||||
self._expires_at = datetime.utcnow() + timedelta(seconds=self._cache_seconds)
|
self._expires_at = datetime.now(UTC) + timedelta(seconds=self._cache_seconds)
|
||||||
else:
|
else:
|
||||||
self._expires_at = None
|
self._expires_at = None
|
||||||
logger.debug('Получен и закеширован публичный ключ WATA')
|
logger.debug('Получен и закеширован публичный ключ WATA')
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import io
|
|||||||
import math
|
import math
|
||||||
import time
|
import time
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
from aiogram import Dispatcher, F, types
|
from aiogram import Dispatcher, F, types
|
||||||
from aiogram.filters import BaseFilter, StateFilter
|
from aiogram.filters import BaseFilter, StateFilter
|
||||||
@@ -742,7 +742,7 @@ async def export_settings(
|
|||||||
keys = sorted(set(keys))
|
keys = sorted(set(keys))
|
||||||
lines = [
|
lines = [
|
||||||
'# RemnaWave bot configuration export',
|
'# RemnaWave bot configuration export',
|
||||||
f'# Generated at {datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC")}',
|
f'# Generated at {datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S UTC")}',
|
||||||
]
|
]
|
||||||
|
|
||||||
for setting_key in keys:
|
for setting_key in keys:
|
||||||
@@ -753,7 +753,7 @@ async def export_settings(
|
|||||||
lines.append(f'{setting_key}={raw_value}')
|
lines.append(f'{setting_key}={raw_value}')
|
||||||
|
|
||||||
content = '\n'.join(lines)
|
content = '\n'.join(lines)
|
||||||
filename = f'bot-settings-{datetime.utcnow().strftime("%Y%m%d-%H%M%S")}.env'
|
filename = f'bot-settings-{datetime.now(UTC).strftime("%Y%m%d-%H%M%S")}.env'
|
||||||
file = types.BufferedInputFile(content.encode('utf-8'), filename=filename)
|
file = types.BufferedInputFile(content.encode('utf-8'), filename=filename)
|
||||||
|
|
||||||
await callback.message.answer_document(
|
await callback.message.answer_document(
|
||||||
|
|||||||
@@ -399,7 +399,7 @@ async def delete_contest(
|
|||||||
await callback.answer(texts.t('ADMIN_CONTEST_NOT_FOUND', 'Конкурс не найден.'), show_alert=True)
|
await callback.answer(texts.t('ADMIN_CONTEST_NOT_FOUND', 'Конкурс не найден.'), show_alert=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
now_utc = datetime.utcnow()
|
now_utc = datetime.now(UTC)
|
||||||
if contest.is_active or contest.end_at > now_utc:
|
if contest.is_active or contest.end_at > now_utc:
|
||||||
await callback.answer(
|
await callback.answer(
|
||||||
texts.t('ADMIN_CONTEST_DELETE_RESTRICT', 'Удалять можно только завершённые конкурсы.'),
|
texts.t('ADMIN_CONTEST_DELETE_RESTRICT', 'Удалять можно только завершённые конкурсы.'),
|
||||||
@@ -628,8 +628,8 @@ async def finalize_contest_creation(message: types.Message, state: FSMContext, d
|
|||||||
await message.answer(texts.t('ADMIN_CONTEST_INVALID_DATE', 'Не удалось распознать дату.'))
|
await message.answer(texts.t('ADMIN_CONTEST_INVALID_DATE', 'Не удалось распознать дату.'))
|
||||||
return
|
return
|
||||||
|
|
||||||
start_at = datetime.fromisoformat(start_at_raw).astimezone(UTC).replace(tzinfo=None)
|
start_at = datetime.fromisoformat(start_at_raw).astimezone(UTC)
|
||||||
end_at = datetime.fromisoformat(end_at_raw).astimezone(UTC).replace(tzinfo=None)
|
end_at = datetime.fromisoformat(end_at_raw).astimezone(UTC)
|
||||||
|
|
||||||
contest_type = data.get('contest_type') or 'referral_paid'
|
contest_type = data.get('contest_type') or 'referral_paid'
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import json
|
import json
|
||||||
from datetime import datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from aiogram import Dispatcher, F, types
|
from aiogram import Dispatcher, F, types
|
||||||
@@ -171,7 +171,7 @@ async def start_round_now(
|
|||||||
await db.refresh(tpl)
|
await db.refresh(tpl)
|
||||||
|
|
||||||
payload = contest_rotation_service._build_payload_for_template(tpl) # type: ignore[attr-defined]
|
payload = contest_rotation_service._build_payload_for_template(tpl) # type: ignore[attr-defined]
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
ends = now + timedelta(hours=tpl.cooldown_hours)
|
ends = now + timedelta(hours=tpl.cooldown_hours)
|
||||||
await create_round(
|
await create_round(
|
||||||
db,
|
db,
|
||||||
@@ -182,8 +182,8 @@ async def start_round_now(
|
|||||||
)
|
)
|
||||||
await contest_rotation_service._announce_round_start( # type: ignore[attr-defined]
|
await contest_rotation_service._announce_round_start( # type: ignore[attr-defined]
|
||||||
tpl,
|
tpl,
|
||||||
now.replace(tzinfo=None),
|
now,
|
||||||
ends.replace(tzinfo=None),
|
ends,
|
||||||
)
|
)
|
||||||
await callback.answer(texts.t('ADMIN_ROUND_STARTED', 'Раунд запущен'), show_alert=True)
|
await callback.answer(texts.t('ADMIN_ROUND_STARTED', 'Раунд запущен'), show_alert=True)
|
||||||
await show_daily_contest(callback, db_user, db)
|
await show_daily_contest(callback, db_user, db)
|
||||||
@@ -214,7 +214,7 @@ async def manual_start_round(
|
|||||||
|
|
||||||
# Для ручного старта не включаем конкурс, если он выключен
|
# Для ручного старта не включаем конкурс, если он выключен
|
||||||
payload = contest_rotation_service._build_payload_for_template(tpl) # type: ignore[attr-defined]
|
payload = contest_rotation_service._build_payload_for_template(tpl) # type: ignore[attr-defined]
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
ends = now + timedelta(hours=tpl.cooldown_hours)
|
ends = now + timedelta(hours=tpl.cooldown_hours)
|
||||||
await create_round(
|
await create_round(
|
||||||
db,
|
db,
|
||||||
@@ -227,8 +227,8 @@ async def manual_start_round(
|
|||||||
# Анонсируем всем пользователям (как тест)
|
# Анонсируем всем пользователям (как тест)
|
||||||
await contest_rotation_service._announce_round_start( # type: ignore[attr-defined]
|
await contest_rotation_service._announce_round_start( # type: ignore[attr-defined]
|
||||||
tpl,
|
tpl,
|
||||||
now.replace(tzinfo=None),
|
now,
|
||||||
ends.replace(tzinfo=None),
|
ends,
|
||||||
)
|
)
|
||||||
await callback.answer(texts.t('ADMIN_ROUND_STARTED', 'Тестовый раунд запущен'), show_alert=True)
|
await callback.answer(texts.t('ADMIN_ROUND_STARTED', 'Тестовый раунд запущен'), show_alert=True)
|
||||||
await show_daily_contest(callback, db_user, db)
|
await show_daily_contest(callback, db_user, db)
|
||||||
@@ -431,7 +431,7 @@ async def start_all_contests(
|
|||||||
continue # уже запущен
|
continue # уже запущен
|
||||||
|
|
||||||
payload = contest_rotation_service._build_payload_for_template(tpl) # type: ignore[attr-defined]
|
payload = contest_rotation_service._build_payload_for_template(tpl) # type: ignore[attr-defined]
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
ends = now + timedelta(hours=tpl.cooldown_hours)
|
ends = now + timedelta(hours=tpl.cooldown_hours)
|
||||||
await create_round(
|
await create_round(
|
||||||
db,
|
db,
|
||||||
@@ -442,8 +442,8 @@ async def start_all_contests(
|
|||||||
)
|
)
|
||||||
await contest_rotation_service._announce_round_start( # type: ignore[attr-defined]
|
await contest_rotation_service._announce_round_start( # type: ignore[attr-defined]
|
||||||
tpl,
|
tpl,
|
||||||
now.replace(tzinfo=None),
|
now,
|
||||||
ends.replace(tzinfo=None),
|
ends,
|
||||||
)
|
)
|
||||||
started_count += 1
|
started_count += 1
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import html
|
import html
|
||||||
from datetime import datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from aiogram import Dispatcher, F, types
|
from aiogram import Dispatcher, F, types
|
||||||
@@ -148,7 +148,7 @@ async def _persist_broadcast_result(
|
|||||||
failed_count: Количество неудачных отправок
|
failed_count: Количество неудачных отправок
|
||||||
status: Финальный статус рассылки ('completed', 'partial', 'failed')
|
status: Финальный статус рассылки ('completed', 'partial', 'failed')
|
||||||
"""
|
"""
|
||||||
completed_at = datetime.utcnow()
|
completed_at = datetime.now(UTC)
|
||||||
max_retries = 3
|
max_retries = 3
|
||||||
retry_delay = 1.0
|
retry_delay = 1.0
|
||||||
|
|
||||||
@@ -309,7 +309,7 @@ async def toggle_pinned_message_position(
|
|||||||
return
|
return
|
||||||
|
|
||||||
pinned_message.send_before_menu = not pinned_message.send_before_menu
|
pinned_message.send_before_menu = not pinned_message.send_before_menu
|
||||||
pinned_message.updated_at = datetime.utcnow()
|
pinned_message.updated_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
await show_pinned_message_menu(callback, db_user, db, state)
|
await show_pinned_message_menu(callback, db_user, db, state)
|
||||||
@@ -329,7 +329,7 @@ async def toggle_pinned_message_start_mode(
|
|||||||
return
|
return
|
||||||
|
|
||||||
pinned_message.send_on_every_start = not pinned_message.send_on_every_start
|
pinned_message.send_on_every_start = not pinned_message.send_on_every_start
|
||||||
pinned_message.updated_at = datetime.utcnow()
|
pinned_message.updated_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
await show_pinned_message_menu(callback, db_user, db, state)
|
await show_pinned_message_menu(callback, db_user, db, state)
|
||||||
@@ -1449,7 +1449,7 @@ async def confirm_broadcast(callback: types.CallbackQuery, db_user: User, state:
|
|||||||
|
|
||||||
async def get_target_users_count(db: AsyncSession, target: str) -> int:
|
async def get_target_users_count(db: AsyncSession, target: str) -> int:
|
||||||
"""Быстрый подсчёт пользователей через SQL COUNT вместо загрузки всех в память."""
|
"""Быстрый подсчёт пользователей через SQL COUNT вместо загрузки всех в память."""
|
||||||
from datetime import datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
from sqlalchemy import distinct, func as sql_func
|
from sqlalchemy import distinct, func as sql_func
|
||||||
|
|
||||||
@@ -1503,7 +1503,7 @@ async def get_target_users_count(db: AsyncSession, target: str) -> int:
|
|||||||
|
|
||||||
if target == 'expiring':
|
if target == 'expiring':
|
||||||
# Истекающие в ближайшие 3 дня
|
# Истекающие в ближайшие 3 дня
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
expiry_threshold = now + timedelta(days=3)
|
expiry_threshold = now + timedelta(days=3)
|
||||||
query = (
|
query = (
|
||||||
select(sql_func.count(distinct(User.id)))
|
select(sql_func.count(distinct(User.id)))
|
||||||
@@ -1520,7 +1520,7 @@ async def get_target_users_count(db: AsyncSession, target: str) -> int:
|
|||||||
|
|
||||||
if target == 'expiring_subscribers':
|
if target == 'expiring_subscribers':
|
||||||
# Истекающие в ближайшие 7 дней
|
# Истекающие в ближайшие 7 дней
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
expiry_threshold = now + timedelta(days=7)
|
expiry_threshold = now + timedelta(days=7)
|
||||||
query = (
|
query = (
|
||||||
select(sql_func.count(distinct(User.id)))
|
select(sql_func.count(distinct(User.id)))
|
||||||
@@ -1537,7 +1537,7 @@ async def get_target_users_count(db: AsyncSession, target: str) -> int:
|
|||||||
|
|
||||||
if target == 'expired':
|
if target == 'expired':
|
||||||
# Истекшие подписки
|
# Истекшие подписки
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
expired_statuses = [SubscriptionStatus.EXPIRED.value, SubscriptionStatus.DISABLED.value]
|
expired_statuses = [SubscriptionStatus.EXPIRED.value, SubscriptionStatus.DISABLED.value]
|
||||||
query = (
|
query = (
|
||||||
select(sql_func.count(distinct(User.id)))
|
select(sql_func.count(distinct(User.id)))
|
||||||
@@ -1556,7 +1556,7 @@ async def get_target_users_count(db: AsyncSession, target: str) -> int:
|
|||||||
|
|
||||||
if target == 'expired_subscribers':
|
if target == 'expired_subscribers':
|
||||||
# То же что и expired
|
# То же что и expired
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
expired_statuses = [SubscriptionStatus.EXPIRED.value, SubscriptionStatus.DISABLED.value]
|
expired_statuses = [SubscriptionStatus.EXPIRED.value, SubscriptionStatus.DISABLED.value]
|
||||||
query = (
|
query = (
|
||||||
select(sql_func.count(distinct(User.id)))
|
select(sql_func.count(distinct(User.id)))
|
||||||
@@ -1634,7 +1634,7 @@ async def get_target_users_count(db: AsyncSession, target: str) -> int:
|
|||||||
|
|
||||||
# Custom filters — быстрый COUNT вместо загрузки всех пользователей
|
# Custom filters — быстрый COUNT вместо загрузки всех пользователей
|
||||||
if target.startswith('custom_'):
|
if target.startswith('custom_'):
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
today = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
today = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
criteria = target[len('custom_') :]
|
criteria = target[len('custom_') :]
|
||||||
|
|
||||||
@@ -1704,7 +1704,7 @@ async def get_target_users(db: AsyncSession, target: str) -> list:
|
|||||||
return [sub.user for sub in expiring_subs if sub.user]
|
return [sub.user for sub in expiring_subs if sub.user]
|
||||||
|
|
||||||
if target == 'expired':
|
if target == 'expired':
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
expired_statuses = {
|
expired_statuses = {
|
||||||
SubscriptionStatus.EXPIRED.value,
|
SubscriptionStatus.EXPIRED.value,
|
||||||
SubscriptionStatus.DISABLED.value,
|
SubscriptionStatus.DISABLED.value,
|
||||||
@@ -1755,7 +1755,7 @@ async def get_target_users(db: AsyncSession, target: str) -> list:
|
|||||||
return [sub.user for sub in expiring_subs if sub.user]
|
return [sub.user for sub in expiring_subs if sub.user]
|
||||||
|
|
||||||
if target == 'expired_subscribers':
|
if target == 'expired_subscribers':
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
expired_statuses = {
|
expired_statuses = {
|
||||||
SubscriptionStatus.EXPIRED.value,
|
SubscriptionStatus.EXPIRED.value,
|
||||||
SubscriptionStatus.DISABLED.value,
|
SubscriptionStatus.DISABLED.value,
|
||||||
@@ -1782,7 +1782,7 @@ async def get_target_users(db: AsyncSession, target: str) -> list:
|
|||||||
]
|
]
|
||||||
|
|
||||||
if target == 'trial_ending':
|
if target == 'trial_ending':
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
in_3_days = now + timedelta(days=3)
|
in_3_days = now + timedelta(days=3)
|
||||||
return [
|
return [
|
||||||
user
|
user
|
||||||
@@ -1794,7 +1794,7 @@ async def get_target_users(db: AsyncSession, target: str) -> list:
|
|||||||
]
|
]
|
||||||
|
|
||||||
if target == 'trial_expired':
|
if target == 'trial_expired':
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
return [
|
return [
|
||||||
user
|
user
|
||||||
for user in users
|
for user in users
|
||||||
@@ -1804,7 +1804,7 @@ async def get_target_users(db: AsyncSession, target: str) -> list:
|
|||||||
if target == 'autopay_failed':
|
if target == 'autopay_failed':
|
||||||
from app.database.models import SubscriptionEvent
|
from app.database.models import SubscriptionEvent
|
||||||
|
|
||||||
week_ago = datetime.utcnow() - timedelta(days=7)
|
week_ago = datetime.now(UTC) - timedelta(days=7)
|
||||||
stmt = (
|
stmt = (
|
||||||
select(SubscriptionEvent.user_id)
|
select(SubscriptionEvent.user_id)
|
||||||
.where(
|
.where(
|
||||||
@@ -1826,15 +1826,15 @@ async def get_target_users(db: AsyncSession, target: str) -> list:
|
|||||||
]
|
]
|
||||||
|
|
||||||
if target == 'inactive_30d':
|
if target == 'inactive_30d':
|
||||||
threshold = datetime.utcnow() - timedelta(days=30)
|
threshold = datetime.now(UTC) - timedelta(days=30)
|
||||||
return [user for user in users if user.last_activity and user.last_activity < threshold]
|
return [user for user in users if user.last_activity and user.last_activity < threshold]
|
||||||
|
|
||||||
if target == 'inactive_60d':
|
if target == 'inactive_60d':
|
||||||
threshold = datetime.utcnow() - timedelta(days=60)
|
threshold = datetime.now(UTC) - timedelta(days=60)
|
||||||
return [user for user in users if user.last_activity and user.last_activity < threshold]
|
return [user for user in users if user.last_activity and user.last_activity < threshold]
|
||||||
|
|
||||||
if target == 'inactive_90d':
|
if target == 'inactive_90d':
|
||||||
threshold = datetime.utcnow() - timedelta(days=90)
|
threshold = datetime.now(UTC) - timedelta(days=90)
|
||||||
return [user for user in users if user.last_activity and user.last_activity < threshold]
|
return [user for user in users if user.last_activity and user.last_activity < threshold]
|
||||||
|
|
||||||
# Фильтр по тарифу
|
# Фильтр по тарифу
|
||||||
@@ -1855,7 +1855,7 @@ async def get_custom_users_count(db: AsyncSession, criteria: str) -> int:
|
|||||||
|
|
||||||
|
|
||||||
async def get_custom_users(db: AsyncSession, criteria: str) -> list:
|
async def get_custom_users(db: AsyncSession, criteria: str) -> list:
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
today = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
today = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
week_ago = now - timedelta(days=7)
|
week_ago = now - timedelta(days=7)
|
||||||
month_ago = now - timedelta(days=30)
|
month_ago = now - timedelta(days=30)
|
||||||
@@ -1884,7 +1884,7 @@ async def get_custom_users(db: AsyncSession, criteria: str) -> list:
|
|||||||
|
|
||||||
|
|
||||||
async def get_users_statistics(db: AsyncSession) -> dict:
|
async def get_users_statistics(db: AsyncSession) -> dict:
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
today = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
today = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
week_ago = now - timedelta(days=7)
|
week_ago = now - timedelta(days=7)
|
||||||
month_ago = now - timedelta(days=30)
|
month_ago = now - timedelta(days=30)
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ def _build_notification_settings_view(language: str):
|
|||||||
|
|
||||||
def _build_notification_preview_message(language: str, notification_type: str):
|
def _build_notification_preview_message(language: str, notification_type: str):
|
||||||
texts = get_texts(language)
|
texts = get_texts(language)
|
||||||
now = datetime.now()
|
now = datetime.now(UTC)
|
||||||
price_30_days = settings.format_price(settings.PRICE_30_DAYS)
|
price_30_days = settings.format_price(settings.PRICE_30_DAYS)
|
||||||
|
|
||||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||||
@@ -694,7 +694,7 @@ async def force_check_callback(callback: CallbackQuery):
|
|||||||
• Истекающих подписок: {results['expiring']}
|
• Истекающих подписок: {results['expiring']}
|
||||||
• Готовых к автооплате: {results['autopay_ready']}
|
• Готовых к автооплате: {results['autopay_ready']}
|
||||||
|
|
||||||
🕐 <b>Время проверки:</b> {datetime.now().strftime('%H:%M:%S')}
|
🕐 <b>Время проверки:</b> {datetime.now(UTC).strftime('%H:%M:%S')}
|
||||||
|
|
||||||
Нажмите "Назад" для возврата в меню мониторинга.
|
Нажмите "Назад" для возврата в меню мониторинга.
|
||||||
"""
|
"""
|
||||||
@@ -748,7 +748,7 @@ async def traffic_check_callback(callback: CallbackQuery):
|
|||||||
• Порог дельты: {threshold_gb} ГБ
|
• Порог дельты: {threshold_gb} ГБ
|
||||||
• Возраст snapshot: {snapshot_age:.1f} мин
|
• Возраст snapshot: {snapshot_age:.1f} мин
|
||||||
|
|
||||||
🕐 <b>Время проверки:</b> {datetime.now().strftime('%H:%M:%S')}
|
🕐 <b>Время проверки:</b> {datetime.now(UTC).strftime('%H:%M:%S')}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if violations:
|
if violations:
|
||||||
@@ -862,7 +862,7 @@ async def test_notifications_callback(callback: CallbackQuery):
|
|||||||
📊 <b>Статус системы:</b>
|
📊 <b>Статус системы:</b>
|
||||||
• Мониторинг: {'🟢 Работает' if monitoring_service.is_running else '🔴 Остановлен'}
|
• Мониторинг: {'🟢 Работает' if monitoring_service.is_running else '🔴 Остановлен'}
|
||||||
• Уведомления: {'🟢 Включены' if settings.ENABLE_NOTIFICATIONS else '🔴 Отключены'}
|
• Уведомления: {'🟢 Включены' if settings.ENABLE_NOTIFICATIONS else '🔴 Отключены'}
|
||||||
• Время теста: {datetime.now().strftime('%H:%M:%S %d.%m.%Y')}
|
• Время теста: {datetime.now(UTC).strftime('%H:%M:%S %d.%m.%Y')}
|
||||||
|
|
||||||
✅ Если вы получили это сообщение, система уведомлений работает корректно!
|
✅ Если вы получили это сообщение, система уведомлений работает корректно!
|
||||||
"""
|
"""
|
||||||
@@ -887,7 +887,7 @@ async def monitoring_statistics_callback(callback: CallbackQuery):
|
|||||||
|
|
||||||
mon_status = await monitoring_service.get_monitoring_status(db)
|
mon_status = await monitoring_service.get_monitoring_status(db)
|
||||||
|
|
||||||
week_ago = datetime.now() - timedelta(days=7)
|
week_ago = datetime.now(UTC) - timedelta(days=7)
|
||||||
week_logs = await monitoring_service.get_monitoring_logs(db, limit=1000)
|
week_logs = await monitoring_service.get_monitoring_logs(db, limit=1000)
|
||||||
week_logs = [log for log in week_logs if log['created_at'] >= week_ago]
|
week_logs = [log for log in week_logs if log['created_at'] >= week_ago]
|
||||||
|
|
||||||
@@ -1013,7 +1013,7 @@ async def nalogo_force_process_callback(callback: CallbackQuery):
|
|||||||
sub_stats = await get_subscriptions_statistics(db)
|
sub_stats = await get_subscriptions_statistics(db)
|
||||||
mon_status = await monitoring_service.get_monitoring_status(db)
|
mon_status = await monitoring_service.get_monitoring_status(db)
|
||||||
|
|
||||||
week_ago = datetime.now() - timedelta(days=7)
|
week_ago = datetime.now(UTC) - timedelta(days=7)
|
||||||
week_logs = await monitoring_service.get_monitoring_logs(db, limit=1000)
|
week_logs = await monitoring_service.get_monitoring_logs(db, limit=1000)
|
||||||
week_logs = [log for log in week_logs if log['created_at'] >= week_ago]
|
week_logs = [log for log in week_logs if log['created_at'] >= week_ago]
|
||||||
week_success = sum(1 for log in week_logs if log['is_success'])
|
week_success = sum(1 for log in week_logs if log['is_success'])
|
||||||
@@ -1235,7 +1235,7 @@ async def receipts_missing_callback(callback: CallbackQuery):
|
|||||||
async def receipts_link_old_callback(callback: CallbackQuery):
|
async def receipts_link_old_callback(callback: CallbackQuery):
|
||||||
"""Привязать старые чеки из NaloGO к транзакциям по сумме и дате."""
|
"""Привязать старые чеки из NaloGO к транзакциям по сумме и дате."""
|
||||||
try:
|
try:
|
||||||
from datetime import date, timedelta
|
from datetime import UTC, date, timedelta
|
||||||
|
|
||||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||||
from sqlalchemy import and_, select
|
from sqlalchemy import and_, select
|
||||||
@@ -1245,7 +1245,7 @@ async def receipts_link_old_callback(callback: CallbackQuery):
|
|||||||
|
|
||||||
await callback.answer('🔄 Загружаю чеки из NaloGO...', show_alert=False)
|
await callback.answer('🔄 Загружаю чеки из NaloGO...', show_alert=False)
|
||||||
|
|
||||||
TRACKING_START_DATE = datetime(2024, 12, 29, 0, 0, 0)
|
TRACKING_START_DATE = datetime(2024, 12, 29, 0, 0, 0, tzinfo=UTC)
|
||||||
|
|
||||||
async with AsyncSessionLocal() as db:
|
async with AsyncSessionLocal() as db:
|
||||||
# Получаем старые транзакции без чеков
|
# Получаем старые транзакции без чеков
|
||||||
@@ -1311,9 +1311,12 @@ async def receipts_link_old_callback(callback: CallbackQuery):
|
|||||||
try:
|
try:
|
||||||
from dateutil.parser import isoparse
|
from dateutil.parser import isoparse
|
||||||
|
|
||||||
t.receipt_created_at = isoparse(operation_time)
|
parsed_time = isoparse(operation_time)
|
||||||
|
t.receipt_created_at = (
|
||||||
|
parsed_time if parsed_time.tzinfo else parsed_time.replace(tzinfo=UTC)
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
t.receipt_created_at = datetime.utcnow()
|
t.receipt_created_at = datetime.now(UTC)
|
||||||
linked += 1
|
linked += 1
|
||||||
|
|
||||||
if linked > 0:
|
if linked > 0:
|
||||||
|
|||||||
@@ -822,9 +822,9 @@ async def export_payments(
|
|||||||
file_bytes = json_content.encode('utf-8')
|
file_bytes = json_content.encode('utf-8')
|
||||||
|
|
||||||
# Отправляем файл
|
# Отправляем файл
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
filename = f'payments_export_{datetime.now().strftime("%Y%m%d_%H%M%S")}.json'
|
filename = f'payments_export_{datetime.now(UTC).strftime("%Y%m%d_%H%M%S")}.json'
|
||||||
|
|
||||||
await callback.message.answer_document(
|
await callback.message.answer_document(
|
||||||
document=BufferedInputFile(file_bytes, filename=filename),
|
document=BufferedInputFile(file_bytes, filename=filename),
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import asyncio
|
|||||||
import html
|
import html
|
||||||
import re
|
import re
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from aiogram import Dispatcher, F, types
|
from aiogram import Dispatcher, F, types
|
||||||
@@ -260,7 +260,7 @@ def _format_offer_remaining(offer, texts) -> str:
|
|||||||
if not offer.expires_at:
|
if not offer.expires_at:
|
||||||
return texts.t('ADMIN_PROMO_OFFER_SEND_USER_OFFER_NO_EXPIRY', 'без срока')
|
return texts.t('ADMIN_PROMO_OFFER_SEND_USER_OFFER_NO_EXPIRY', 'без срока')
|
||||||
|
|
||||||
remaining_seconds = int((offer.expires_at - datetime.utcnow()).total_seconds())
|
remaining_seconds = int((offer.expires_at - datetime.now(UTC)).total_seconds())
|
||||||
if remaining_seconds <= 0:
|
if remaining_seconds <= 0:
|
||||||
return texts.t('ADMIN_PROMO_OFFER_SEND_USER_OFFER_TIME_LEFT_EXPIRED', 'истекло')
|
return texts.t('ADMIN_PROMO_OFFER_SEND_USER_OFFER_TIME_LEFT_EXPIRED', 'истекло')
|
||||||
|
|
||||||
@@ -1512,7 +1512,7 @@ async def show_selected_user_details(
|
|||||||
lines.append('')
|
lines.append('')
|
||||||
lines.append(texts.t('ADMIN_PROMO_OFFER_SEND_USER_NO_SUBSCRIPTION', '💳 Подписка отсутствует'))
|
lines.append(texts.t('ADMIN_PROMO_OFFER_SEND_USER_NO_SUBSCRIPTION', '💳 Подписка отсутствует'))
|
||||||
|
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
percent = 0
|
percent = 0
|
||||||
try:
|
try:
|
||||||
percent = int(getattr(user, 'promo_offer_discount_percent', 0) or 0)
|
percent = int(getattr(user, 'promo_offer_discount_percent', 0) or 0)
|
||||||
@@ -1772,7 +1772,7 @@ async def show_selected_user_details(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if subscription:
|
if subscription:
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(SubscriptionTemporaryAccess)
|
select(SubscriptionTemporaryAccess)
|
||||||
.options(selectinload(SubscriptionTemporaryAccess.offer))
|
.options(selectinload(SubscriptionTemporaryAccess.offer))
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from aiogram import Dispatcher, F, types
|
from aiogram import Dispatcher, F, types
|
||||||
@@ -722,7 +722,7 @@ async def process_promocode_expiry(message: types.Message, db_user: User, state:
|
|||||||
|
|
||||||
valid_until = None
|
valid_until = None
|
||||||
if expiry_days > 0:
|
if expiry_days > 0:
|
||||||
valid_until = datetime.utcnow() + timedelta(days=expiry_days)
|
valid_until = datetime.now(UTC) + timedelta(days=expiry_days)
|
||||||
|
|
||||||
type_map = {
|
type_map = {
|
||||||
'balance': PromoCodeType.BALANCE,
|
'balance': PromoCodeType.BALANCE,
|
||||||
@@ -803,7 +803,7 @@ async def process_discount_hours(message: types.Message, db_user: User, state: F
|
|||||||
|
|
||||||
valid_until = None
|
valid_until = None
|
||||||
if expiry_days > 0:
|
if expiry_days > 0:
|
||||||
valid_until = datetime.utcnow() + timedelta(days=expiry_days)
|
valid_until = datetime.now(UTC) + timedelta(days=expiry_days)
|
||||||
|
|
||||||
# Создаем DISCOUNT промокод
|
# Создаем DISCOUNT промокод
|
||||||
# balance_bonus_kopeks = процент скидки (НЕ копейки!)
|
# balance_bonus_kopeks = процент скидки (НЕ копейки!)
|
||||||
@@ -877,7 +877,7 @@ async def handle_edit_expiry(message: types.Message, db_user: User, state: FSMCo
|
|||||||
|
|
||||||
valid_until = None
|
valid_until = None
|
||||||
if expiry_days > 0:
|
if expiry_days > 0:
|
||||||
valid_until = datetime.utcnow() + timedelta(days=expiry_days)
|
valid_until = datetime.now(UTC) + timedelta(days=expiry_days)
|
||||||
|
|
||||||
await update_promocode(db, promo, valid_until=valid_until)
|
await update_promocode(db, promo, valid_until=valid_until)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
|
from datetime import UTC
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from aiogram import Dispatcher, F, types
|
from aiogram import Dispatcher, F, types
|
||||||
@@ -33,7 +34,7 @@ async def show_referral_statistics(callback: types.CallbackQuery, db_user: User,
|
|||||||
if stats.get('active_referrers', 0) > 0:
|
if stats.get('active_referrers', 0) > 0:
|
||||||
avg_per_referrer = stats.get('total_paid_kopeks', 0) / stats['active_referrers']
|
avg_per_referrer = stats.get('total_paid_kopeks', 0) / stats['active_referrers']
|
||||||
|
|
||||||
current_time = datetime.datetime.now().strftime('%H:%M:%S')
|
current_time = datetime.datetime.now(UTC).strftime('%H:%M:%S')
|
||||||
|
|
||||||
text = f"""
|
text = f"""
|
||||||
🤝 <b>Реферальная статистика</b>
|
🤝 <b>Реферальная статистика</b>
|
||||||
@@ -114,7 +115,7 @@ async def show_referral_statistics(callback: types.CallbackQuery, db_user: User,
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error('Ошибка в show_referral_statistics', error=e, exc_info=True)
|
logger.error('Ошибка в show_referral_statistics', error=e, exc_info=True)
|
||||||
|
|
||||||
current_time = datetime.datetime.now().strftime('%H:%M:%S')
|
current_time = datetime.datetime.now(UTC).strftime('%H:%M:%S')
|
||||||
text = f"""
|
text = f"""
|
||||||
🤝 <b>Реферальная статистика</b>
|
🤝 <b>Реферальная статистика</b>
|
||||||
|
|
||||||
@@ -656,7 +657,7 @@ async def process_test_referral_earning(message: types.Message, db_user: User, d
|
|||||||
|
|
||||||
def _get_period_dates(period: str) -> tuple[datetime.datetime, datetime.datetime]:
|
def _get_period_dates(period: str) -> tuple[datetime.datetime, datetime.datetime]:
|
||||||
"""Возвращает начальную и конечную даты для заданного периода."""
|
"""Возвращает начальную и конечную даты для заданного периода."""
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now(UTC)
|
||||||
today = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
today = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
|
||||||
if period == 'today':
|
if period == 'today':
|
||||||
@@ -1153,9 +1154,9 @@ async def sync_referrals_with_contest(
|
|||||||
await callback.answer('🏆 Синхронизирую с конкурсами...')
|
await callback.answer('🏆 Синхронизирую с конкурсами...')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
now_utc = datetime.utcnow()
|
now_utc = datetime.now(UTC)
|
||||||
|
|
||||||
# Получаем активные конкурсы
|
# Получаем активные конкурсы
|
||||||
paid_contests = await get_contests_for_events(db, now_utc, contest_types=['referral_paid'])
|
paid_contests = await get_contests_for_events(db, now_utc, contest_types=['referral_paid'])
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import math
|
import math
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
@@ -1041,7 +1041,7 @@ async def show_system_stats(callback: types.CallbackQuery, db_user: User, db: As
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
text += f"""
|
text += f"""
|
||||||
🕒 <b>Обновлено:</b> {format_datetime(stats.get('last_updated', datetime.now()))}
|
🕒 <b>Обновлено:</b> {format_datetime(stats.get('last_updated', datetime.now(UTC)))}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
keyboard = [
|
keyboard = [
|
||||||
@@ -1167,7 +1167,7 @@ async def show_traffic_stats(callback: types.CallbackQuery, db_user: User, db: A
|
|||||||
for i, (node_name, total_bytes) in enumerate(sorted_nodes[:5], 1):
|
for i, (node_name, total_bytes) in enumerate(sorted_nodes[:5], 1):
|
||||||
text += f'{i}. {node_name}: {format_bytes(total_bytes)}\n'
|
text += f'{i}. {node_name}: {format_bytes(total_bytes)}\n'
|
||||||
|
|
||||||
text += f'\n🕒 <b>Обновлено:</b> {format_datetime(datetime.now())}'
|
text += f'\n🕒 <b>Обновлено:</b> {format_datetime(datetime.now(UTC))}'
|
||||||
|
|
||||||
keyboard = [
|
keyboard = [
|
||||||
[types.InlineKeyboardButton(text='🔄 Обновить', callback_data='admin_rw_traffic')],
|
[types.InlineKeyboardButton(text='🔄 Обновить', callback_data='admin_rw_traffic')],
|
||||||
@@ -1317,7 +1317,7 @@ async def show_node_statistics(callback: types.CallbackQuery, db_user: User, db:
|
|||||||
try:
|
try:
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
end_date = datetime.now()
|
end_date = datetime.now(UTC)
|
||||||
start_date = end_date - timedelta(days=7)
|
start_date = end_date - timedelta(days=7)
|
||||||
|
|
||||||
node_usage = await remnawave_service.get_node_user_usage_by_range(node_uuid, start_date, end_date)
|
node_usage = await remnawave_service.get_node_user_usage_by_range(node_uuid, start_date, end_date)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from aiogram import Dispatcher, F, types
|
from aiogram import Dispatcher, F, types
|
||||||
@@ -40,7 +40,7 @@ async def show_users_statistics(callback: types.CallbackQuery, db_user: User, db
|
|||||||
total_users = stats['total_users']
|
total_users = stats['total_users']
|
||||||
active_rate = format_percentage(stats['active_users'] / total_users * 100 if total_users > 0 else 0)
|
active_rate = format_percentage(stats['active_users'] / total_users * 100 if total_users > 0 else 0)
|
||||||
|
|
||||||
current_time = format_datetime(datetime.utcnow())
|
current_time = format_datetime(datetime.now(UTC))
|
||||||
|
|
||||||
text = f"""
|
text = f"""
|
||||||
👥 <b>Статистика пользователей</b>
|
👥 <b>Статистика пользователей</b>
|
||||||
@@ -89,7 +89,7 @@ async def show_subscriptions_statistics(callback: types.CallbackQuery, db_user:
|
|||||||
|
|
||||||
total_subs = stats['total_subscriptions']
|
total_subs = stats['total_subscriptions']
|
||||||
conversion_rate = format_percentage(stats['paid_subscriptions'] / total_subs * 100 if total_subs > 0 else 0)
|
conversion_rate = format_percentage(stats['paid_subscriptions'] / total_subs * 100 if total_subs > 0 else 0)
|
||||||
current_time = format_datetime(datetime.utcnow())
|
current_time = format_datetime(datetime.now(UTC))
|
||||||
|
|
||||||
text = f"""
|
text = f"""
|
||||||
📱 <b>Статистика подписок</b>
|
📱 <b>Статистика подписок</b>
|
||||||
@@ -133,12 +133,12 @@ async def show_subscriptions_statistics(callback: types.CallbackQuery, db_user:
|
|||||||
@admin_required
|
@admin_required
|
||||||
@error_handler
|
@error_handler
|
||||||
async def show_revenue_statistics(callback: types.CallbackQuery, db_user: User, db: AsyncSession):
|
async def show_revenue_statistics(callback: types.CallbackQuery, db_user: User, db: AsyncSession):
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
|
||||||
month_stats = await get_transactions_statistics(db, month_start, now)
|
month_stats = await get_transactions_statistics(db, month_start, now)
|
||||||
all_time_stats = await get_transactions_statistics(db)
|
all_time_stats = await get_transactions_statistics(db)
|
||||||
current_time = format_datetime(datetime.utcnow())
|
current_time = format_datetime(datetime.now(UTC))
|
||||||
|
|
||||||
text = f"""
|
text = f"""
|
||||||
💰 <b>Статистика доходов</b>
|
💰 <b>Статистика доходов</b>
|
||||||
@@ -189,7 +189,7 @@ async def show_revenue_statistics(callback: types.CallbackQuery, db_user: User,
|
|||||||
@error_handler
|
@error_handler
|
||||||
async def show_referral_statistics(callback: types.CallbackQuery, db_user: User, db: AsyncSession):
|
async def show_referral_statistics(callback: types.CallbackQuery, db_user: User, db: AsyncSession):
|
||||||
stats = await get_referral_statistics(db)
|
stats = await get_referral_statistics(db)
|
||||||
current_time = format_datetime(datetime.utcnow())
|
current_time = format_datetime(datetime.now(UTC))
|
||||||
|
|
||||||
avg_per_referrer = 0
|
avg_per_referrer = 0
|
||||||
if stats['active_referrers'] > 0:
|
if stats['active_referrers'] > 0:
|
||||||
@@ -250,10 +250,10 @@ async def show_summary_statistics(callback: types.CallbackQuery, db_user: User,
|
|||||||
user_stats = await user_service.get_user_statistics(db)
|
user_stats = await user_service.get_user_statistics(db)
|
||||||
sub_stats = await get_subscriptions_statistics(db)
|
sub_stats = await get_subscriptions_statistics(db)
|
||||||
|
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||||
revenue_stats = await get_transactions_statistics(db, month_start, now)
|
revenue_stats = await get_transactions_statistics(db, month_start, now)
|
||||||
current_time = format_datetime(datetime.utcnow())
|
current_time = format_datetime(datetime.now(UTC))
|
||||||
|
|
||||||
conversion_rate = 0
|
conversion_rate = 0
|
||||||
if user_stats['total_users'] > 0:
|
if user_stats['total_users'] > 0:
|
||||||
@@ -317,10 +317,10 @@ async def show_revenue_by_period(callback: types.CallbackQuery, db_user: User, d
|
|||||||
revenue_data = await get_revenue_by_period(db, days)
|
revenue_data = await get_revenue_by_period(db, days)
|
||||||
|
|
||||||
if period == 'yesterday':
|
if period == 'yesterday':
|
||||||
yesterday = datetime.utcnow().date() - timedelta(days=1)
|
yesterday = datetime.now(UTC).date() - timedelta(days=1)
|
||||||
revenue_data = [r for r in revenue_data if r['date'] == yesterday]
|
revenue_data = [r for r in revenue_data if r['date'] == yesterday]
|
||||||
elif period == 'today':
|
elif period == 'today':
|
||||||
today = datetime.utcnow().date()
|
today = datetime.now(UTC).date()
|
||||||
revenue_data = [r for r in revenue_data if r['date'] == today]
|
revenue_data = [r for r in revenue_data if r['date'] == today]
|
||||||
|
|
||||||
total_revenue = sum(r['amount_kopeks'] for r in revenue_data)
|
total_revenue = sum(r['amount_kopeks'] for r in revenue_data)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
from html import escape
|
from html import escape
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ def _build_logs_message(log_path: Path) -> str:
|
|||||||
|
|
||||||
total_length = len(content)
|
total_length = len(content)
|
||||||
stats = log_path.stat()
|
stats = log_path.stat()
|
||||||
updated_at = datetime.fromtimestamp(stats.st_mtime)
|
updated_at = datetime.fromtimestamp(stats.st_mtime, tz=UTC)
|
||||||
|
|
||||||
if not content:
|
if not content:
|
||||||
preview_text = 'Лог-файл пуст.'
|
preview_text = 'Лог-файл пуст.'
|
||||||
@@ -128,7 +128,7 @@ async def download_system_logs(
|
|||||||
|
|
||||||
document = FSInputFile(log_path)
|
document = FSInputFile(log_path)
|
||||||
stats = log_path.stat()
|
stats = log_path.stat()
|
||||||
updated_at = datetime.fromtimestamp(stats.st_mtime).strftime('%d.%m.%Y %H:%M:%S')
|
updated_at = datetime.fromtimestamp(stats.st_mtime, tz=UTC).strftime('%d.%m.%Y %H:%M:%S')
|
||||||
caption = (
|
caption = (
|
||||||
f'🧾 Лог-файл <code>{log_path.name}</code>\n📁 Путь: <code>{log_path}</code>\n🕒 Обновлен: {updated_at}'
|
f'🧾 Лог-файл <code>{log_path.name}</code>\n📁 Путь: <code>{log_path}</code>\n🕒 Обновлен: {updated_at}'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import html
|
import html
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from aiogram import Bot, Dispatcher, F, types
|
from aiogram import Bot, Dispatcher, F, types
|
||||||
@@ -449,7 +449,7 @@ async def handle_admin_ticket_reply(message: types.Message, state: FSMContext, d
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
await message.answer('❌ Введите целое число минут')
|
await message.answer('❌ Введите целое число минут')
|
||||||
return
|
return
|
||||||
until = datetime.utcnow() + timedelta(minutes=minutes)
|
until = datetime.now(UTC) + timedelta(minutes=minutes)
|
||||||
ok = await TicketCRUD.set_user_reply_block(db, ticket_id, permanent=False, until=until)
|
ok = await TicketCRUD.set_user_reply_block(db, ticket_id, permanent=False, until=until)
|
||||||
if ok:
|
if ok:
|
||||||
await message.answer(f'✅ Пользователь заблокирован на {minutes} минут')
|
await message.answer(f'✅ Пользователь заблокирован на {minutes} минут')
|
||||||
@@ -769,7 +769,7 @@ async def handle_admin_block_duration_input(message: types.Message, state: FSMCo
|
|||||||
await state.clear()
|
await state.clear()
|
||||||
return
|
return
|
||||||
|
|
||||||
until = datetime.utcnow() + timedelta(minutes=minutes)
|
until = datetime.now(UTC) + timedelta(minutes=minutes)
|
||||||
ok = await TicketCRUD.set_user_reply_block(db, ticket_id, permanent=False, until=until)
|
ok = await TicketCRUD.set_user_reply_block(db, ticket_id, permanent=False, until=until)
|
||||||
if not ok:
|
if not ok:
|
||||||
await message.answer('❌ Ошибка блокировки')
|
await message.answer('❌ Ошибка блокировки')
|
||||||
|
|||||||
+16
-16
@@ -1,6 +1,6 @@
|
|||||||
import re
|
import re
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -146,7 +146,7 @@ def _build_user_button_text(
|
|||||||
if user.balance_kopeks > 0:
|
if user.balance_kopeks > 0:
|
||||||
button_text += f' | 💰 {settings.format_price(user.balance_kopeks)}'
|
button_text += f' | 💰 {settings.format_price(user.balance_kopeks)}'
|
||||||
if user.subscription and user.subscription.end_date:
|
if user.subscription and user.subscription.end_date:
|
||||||
days_left = (user.subscription.end_date - datetime.utcnow()).days
|
days_left = (user.subscription.end_date - datetime.now(UTC)).days
|
||||||
button_text += f' | 📅 {days_left}д'
|
button_text += f' | 📅 {days_left}д'
|
||||||
|
|
||||||
elif filter_type == UserFilterType.CAMPAIGN:
|
elif filter_type == UserFilterType.CAMPAIGN:
|
||||||
@@ -425,7 +425,7 @@ async def show_users_ready_to_renew(
|
|||||||
text += 'Нажмите на пользователя для управления:'
|
text += 'Нажмите на пользователя для управления:'
|
||||||
|
|
||||||
keyboard = []
|
keyboard = []
|
||||||
current_time = datetime.utcnow()
|
current_time = datetime.now(UTC)
|
||||||
|
|
||||||
for user in users_data['users']:
|
for user in users_data['users']:
|
||||||
subscription = user.subscription
|
subscription = user.subscription
|
||||||
@@ -730,7 +730,7 @@ async def show_users_statistics(callback: types.CallbackQuery, db_user: User, db
|
|||||||
|
|
||||||
from sqlalchemy import func, or_, select
|
from sqlalchemy import func, or_, select
|
||||||
|
|
||||||
current_time = datetime.utcnow()
|
current_time = datetime.now(UTC)
|
||||||
|
|
||||||
active_subscription_query = (
|
active_subscription_query = (
|
||||||
select(func.count(Subscription.id))
|
select(func.count(Subscription.id))
|
||||||
@@ -859,7 +859,7 @@ async def _render_user_subscription_overview(callback: types.CallbackQuery, db:
|
|||||||
text += f'<b>Устройства:</b> {subscription.device_limit}\n'
|
text += f'<b>Устройства:</b> {subscription.device_limit}\n'
|
||||||
|
|
||||||
if subscription.is_active:
|
if subscription.is_active:
|
||||||
days_left = (subscription.end_date - datetime.utcnow()).days
|
days_left = (subscription.end_date - datetime.now(UTC)).days
|
||||||
text += f'<b>Осталось дней:</b> {days_left}\n'
|
text += f'<b>Осталось дней:</b> {days_left}\n'
|
||||||
|
|
||||||
current_squads = subscription.connected_squads or []
|
current_squads = subscription.connected_squads or []
|
||||||
@@ -1558,7 +1558,7 @@ async def _update_referral_commission_percent(
|
|||||||
return False, None
|
return False, None
|
||||||
|
|
||||||
user.referral_commission_percent = percent
|
user.referral_commission_percent = percent
|
||||||
user.updated_at = datetime.utcnow()
|
user.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
@@ -2817,7 +2817,7 @@ async def get_detailed_referral_stats(db: AsyncSession, user_id: int) -> dict:
|
|||||||
earnings_by_referral[referral_id] += earning.amount_kopeks
|
earnings_by_referral[referral_id] += earning.amount_kopeks
|
||||||
|
|
||||||
referrals_detail = []
|
referrals_detail = []
|
||||||
current_time = datetime.utcnow()
|
current_time = datetime.now(UTC)
|
||||||
|
|
||||||
for referral in referrals:
|
for referral in referrals:
|
||||||
earned = earnings_by_referral.get(referral.id, 0)
|
earned = earnings_by_referral.get(referral.id, 0)
|
||||||
@@ -3469,7 +3469,7 @@ async def toggle_user_server(callback: types.CallbackQuery, db_user: User, db: A
|
|||||||
action_text = 'добавлен'
|
action_text = 'добавлен'
|
||||||
|
|
||||||
subscription.connected_squads = current_squads
|
subscription.connected_squads = current_squads
|
||||||
subscription.updated_at = datetime.utcnow()
|
subscription.updated_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(subscription)
|
await db.refresh(subscription)
|
||||||
|
|
||||||
@@ -3872,7 +3872,7 @@ async def _update_user_devices(db: AsyncSession, user_id: int, devices: int, adm
|
|||||||
subscription = user.subscription
|
subscription = user.subscription
|
||||||
old_devices = subscription.device_limit
|
old_devices = subscription.device_limit
|
||||||
subscription.device_limit = devices
|
subscription.device_limit = devices
|
||||||
subscription.updated_at = datetime.utcnow()
|
subscription.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
@@ -3916,7 +3916,7 @@ async def _update_user_traffic(db: AsyncSession, user_id: int, traffic_gb: int,
|
|||||||
subscription = user.subscription
|
subscription = user.subscription
|
||||||
old_traffic = subscription.traffic_limit_gb
|
old_traffic = subscription.traffic_limit_gb
|
||||||
subscription.traffic_limit_gb = traffic_gb
|
subscription.traffic_limit_gb = traffic_gb
|
||||||
subscription.updated_at = datetime.utcnow()
|
subscription.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
@@ -4038,7 +4038,7 @@ async def _deactivate_user_subscription(db: AsyncSession, user_id: int, admin_id
|
|||||||
|
|
||||||
async def _activate_user_subscription(db: AsyncSession, user_id: int, admin_id: int) -> bool:
|
async def _activate_user_subscription(db: AsyncSession, user_id: int, admin_id: int) -> bool:
|
||||||
try:
|
try:
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
from app.database.crud.subscription import get_subscription_by_user_id
|
from app.database.crud.subscription import get_subscription_by_user_id
|
||||||
from app.database.models import SubscriptionStatus
|
from app.database.models import SubscriptionStatus
|
||||||
@@ -4050,8 +4050,8 @@ async def _activate_user_subscription(db: AsyncSession, user_id: int, admin_id:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
subscription.status = SubscriptionStatus.ACTIVE.value
|
subscription.status = SubscriptionStatus.ACTIVE.value
|
||||||
if subscription.end_date <= datetime.utcnow():
|
if subscription.end_date <= datetime.now(UTC):
|
||||||
subscription.end_date = datetime.utcnow() + timedelta(days=1)
|
subscription.end_date = datetime.now(UTC) + timedelta(days=1)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(subscription)
|
await db.refresh(subscription)
|
||||||
@@ -4506,7 +4506,7 @@ async def admin_buy_subscription_execute(callback: types.CallbackQuery, db_user:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if subscription:
|
if subscription:
|
||||||
current_time = datetime.utcnow()
|
current_time = datetime.now(UTC)
|
||||||
bonus_period = timedelta()
|
bonus_period = timedelta()
|
||||||
|
|
||||||
if subscription.is_trial and settings.TRIAL_ADD_REMAINING_DAYS_TO_PAID and subscription.end_date:
|
if subscription.is_trial and settings.TRIAL_ADD_REMAINING_DAYS_TO_PAID and subscription.end_date:
|
||||||
@@ -5102,7 +5102,7 @@ async def _change_subscription_type(db: AsyncSession, user_id: int, new_type: st
|
|||||||
new_type_text = 'триальной' if new_is_trial else 'платной'
|
new_type_text = 'триальной' if new_is_trial else 'платной'
|
||||||
|
|
||||||
subscription.is_trial = new_is_trial
|
subscription.is_trial = new_is_trial
|
||||||
subscription.updated_at = datetime.utcnow()
|
subscription.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
if not new_is_trial and subscription.is_trial:
|
if not new_is_trial and subscription.is_trial:
|
||||||
user = await get_user_by_id(db, user_id)
|
user = await get_user_by_id(db, user_id)
|
||||||
@@ -5306,7 +5306,7 @@ async def confirm_admin_tariff_change(callback: types.CallbackQuery, db_user: Us
|
|||||||
subscription.device_limit = tariff.device_limit
|
subscription.device_limit = tariff.device_limit
|
||||||
subscription.traffic_limit_gb = tariff.traffic_limit_gb
|
subscription.traffic_limit_gb = tariff.traffic_limit_gb
|
||||||
subscription.connected_squads = tariff.allowed_squads or []
|
subscription.connected_squads = tariff.allowed_squads or []
|
||||||
subscription.updated_at = datetime.utcnow()
|
subscription.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
# Сбрасываем докупленный трафик при смене тарифа
|
# Сбрасываем докупленный трафик при смене тарифа
|
||||||
from sqlalchemy import delete as sql_delete
|
from sqlalchemy import delete as sql_delete
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from aiogram import types
|
from aiogram import types
|
||||||
@@ -251,7 +251,7 @@ async def process_heleket_payment_amount(
|
|||||||
await db.execute(
|
await db.execute(
|
||||||
update(payment.__class__)
|
update(payment.__class__)
|
||||||
.where(payment.__class__.id == payment.id)
|
.where(payment.__class__.id == payment.id)
|
||||||
.values(metadata_json=metadata, updated_at=datetime.utcnow())
|
.values(metadata_json=metadata, updated_at=datetime.now(UTC))
|
||||||
)
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
except Exception as error: # pragma: no cover - diagnostics
|
except Exception as error: # pragma: no cover - diagnostics
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import html
|
import html
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
@@ -220,7 +220,7 @@ async def _send_pal24_payment_message(
|
|||||||
await db.execute(
|
await db.execute(
|
||||||
update(payment.__class__)
|
update(payment.__class__)
|
||||||
.where(payment.__class__.id == payment.id)
|
.where(payment.__class__.id == payment.id)
|
||||||
.values(metadata_json=metadata, updated_at=datetime.utcnow())
|
.values(metadata_json=metadata, updated_at=datetime.now(UTC))
|
||||||
)
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
except Exception as error: # pragma: no cover - diagnostics
|
except Exception as error: # pragma: no cover - diagnostics
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from aiogram import types
|
from aiogram import types
|
||||||
@@ -234,7 +234,7 @@ async def process_wata_payment_amount(
|
|||||||
await db.execute(
|
await db.execute(
|
||||||
update(payment.__class__)
|
update(payment.__class__)
|
||||||
.where(payment.__class__.id == payment.id)
|
.where(payment.__class__.id == payment.id)
|
||||||
.values(metadata_json=metadata, updated_at=datetime.utcnow())
|
.values(metadata_json=metadata, updated_at=datetime.now(UTC))
|
||||||
)
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
except Exception as error: # pragma: no cover - diagnostics
|
except Exception as error: # pragma: no cover - diagnostics
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from aiogram import types
|
from aiogram import types
|
||||||
@@ -270,7 +270,7 @@ async def process_yookassa_payment_amount(
|
|||||||
await db.execute(
|
await db.execute(
|
||||||
update(payment.__class__)
|
update(payment.__class__)
|
||||||
.where(payment.__class__.id == payment.id)
|
.where(payment.__class__.id == payment.id)
|
||||||
.values(metadata_json=metadata, updated_at=datetime.utcnow())
|
.values(metadata_json=metadata, updated_at=datetime.now(UTC))
|
||||||
)
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
except Exception as error: # pragma: no cover - диагностический лог
|
except Exception as error: # pragma: no cover - диагностический лог
|
||||||
@@ -509,7 +509,7 @@ async def process_yookassa_sbp_payment_amount(
|
|||||||
await db.execute(
|
await db.execute(
|
||||||
update(payment.__class__)
|
update(payment.__class__)
|
||||||
.where(payment.__class__.id == payment.id)
|
.where(payment.__class__.id == payment.id)
|
||||||
.values(metadata_json=metadata, updated_at=datetime.utcnow())
|
.values(metadata_json=metadata, updated_at=datetime.now(UTC))
|
||||||
)
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
except Exception as error: # pragma: no cover - диагностический лог
|
except Exception as error: # pragma: no cover - диагностический лог
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Contest handlers for daily games."""
|
"""Contest handlers for daily games."""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from aiogram import Dispatcher, F, types
|
from aiogram import Dispatcher, F, types
|
||||||
@@ -34,7 +34,7 @@ _attempt_service = ContestAttemptService()
|
|||||||
def _check_rate_limit(user_id: int, action: str, limit: int = 1, window_seconds: int = 5) -> bool:
|
def _check_rate_limit(user_id: int, action: str, limit: int = 1, window_seconds: int = 5) -> bool:
|
||||||
"""Check if user exceeds rate limit for contest actions."""
|
"""Check if user exceeds rate limit for contest actions."""
|
||||||
key = f'{user_id}_{action}'
|
key = f'{user_id}_{action}'
|
||||||
now = datetime.utcnow().timestamp()
|
now = datetime.now(UTC).timestamp()
|
||||||
|
|
||||||
if key not in _rate_limits:
|
if key not in _rate_limits:
|
||||||
_rate_limits[key] = []
|
_rate_limits[key] = []
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import html
|
import html
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
@@ -165,7 +165,7 @@ async def show_main_menu(
|
|||||||
|
|
||||||
texts = get_texts(db_user.language)
|
texts = get_texts(db_user.language)
|
||||||
|
|
||||||
db_user.last_activity = datetime.utcnow()
|
db_user.last_activity = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
has_active_subscription = bool(db_user.subscription and db_user.subscription.is_active)
|
has_active_subscription = bool(db_user.subscription and db_user.subscription.is_active)
|
||||||
@@ -1074,7 +1074,7 @@ def _get_subscription_status(user: User, texts, is_daily_tariff: bool = False) -
|
|||||||
if not subscription:
|
if not subscription:
|
||||||
return texts.t('SUB_STATUS_NONE', '❌ Отсутствует')
|
return texts.t('SUB_STATUS_NONE', '❌ Отсутствует')
|
||||||
|
|
||||||
current_time = datetime.utcnow()
|
current_time = datetime.now(UTC)
|
||||||
actual_status = (subscription.actual_status or '').lower()
|
actual_status = (subscription.actual_status or '').lower()
|
||||||
end_date = getattr(subscription, 'end_date', None)
|
end_date = getattr(subscription, 'end_date', None)
|
||||||
end_date_text = format_local_datetime(end_date, '%d.%m.%Y') if end_date else None
|
end_date_text = format_local_datetime(end_date, '%d.%m.%Y') if end_date else None
|
||||||
@@ -1257,7 +1257,7 @@ async def handle_activate_button(callback: types.CallbackQuery, db_user: User, d
|
|||||||
subscription = await get_subscription_by_user_id(db, db_user.id)
|
subscription = await get_subscription_by_user_id(db, db_user.id)
|
||||||
|
|
||||||
# Если подписка активна — ничего не делаем
|
# Если подписка активна — ничего не делаем
|
||||||
if subscription and subscription.status == 'ACTIVE' and subscription.end_date > datetime.utcnow():
|
if subscription and subscription.status == 'ACTIVE' and subscription.end_date > datetime.now(UTC):
|
||||||
await callback.answer(
|
await callback.answer(
|
||||||
texts.t('SUBSCRIPTION_ALREADY_ACTIVE', '✅ Подписка уже активна!'),
|
texts.t('SUBSCRIPTION_ALREADY_ACTIVE', '✅ Подписка уже активна!'),
|
||||||
show_alert=True,
|
show_alert=True,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from aiogram import Dispatcher, F, types
|
from aiogram import Dispatcher, F, types
|
||||||
@@ -113,7 +113,7 @@ async def handle_poll_start(
|
|||||||
return
|
return
|
||||||
|
|
||||||
if not response.started_at:
|
if not response.started_at:
|
||||||
response.started_at = datetime.utcnow()
|
response.started_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
index, question = await get_next_question(response)
|
index, question = await get_next_question(response)
|
||||||
@@ -213,7 +213,7 @@ async def handle_poll_answer(
|
|||||||
await callback.answer()
|
await callback.answer()
|
||||||
return
|
return
|
||||||
|
|
||||||
response.completed_at = datetime.utcnow()
|
response.completed_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
reward_amount = await reward_user_for_poll(db, response)
|
reward_amount = await reward_user_for_poll(db, response)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from aiogram import Dispatcher, F, types
|
from aiogram import Dispatcher, F, types
|
||||||
@@ -106,7 +106,7 @@ def _build_status_message(
|
|||||||
offline=len(offline_servers),
|
offline=len(offline_servers),
|
||||||
)
|
)
|
||||||
|
|
||||||
updated_at = datetime.now().strftime('%H:%M:%S')
|
updated_at = datetime.now(UTC).strftime('%H:%M:%S')
|
||||||
|
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Обработчики для простой покупки подписки."""
|
"""Обработчики для простой покупки подписки."""
|
||||||
|
|
||||||
import html
|
import html
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
@@ -137,7 +137,7 @@ async def start_simple_subscription_purchase(
|
|||||||
elif getattr(current_subscription, 'is_trial', False):
|
elif getattr(current_subscription, 'is_trial', False):
|
||||||
# Это тестовая подписка
|
# Это тестовая подписка
|
||||||
try:
|
try:
|
||||||
days_left = max(0, (current_subscription.end_date - datetime.utcnow()).days)
|
days_left = max(0, (current_subscription.end_date - datetime.now(UTC)).days)
|
||||||
except Exception:
|
except Exception:
|
||||||
days_left = 0
|
days_left = 0
|
||||||
key = (
|
key = (
|
||||||
|
|||||||
+20
-20
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from aiogram import Bot, Dispatcher, F, types
|
from aiogram import Bot, Dispatcher, F, types
|
||||||
@@ -439,10 +439,10 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession,
|
|||||||
logger.info('📝 Фамилия обновлена', old_last_name=old_last_name, last_name=user.last_name)
|
logger.info('📝 Фамилия обновлена', old_last_name=old_last_name, last_name=user.last_name)
|
||||||
profile_updated = True
|
profile_updated = True
|
||||||
|
|
||||||
user.last_activity = datetime.utcnow()
|
user.last_activity = datetime.now(UTC)
|
||||||
|
|
||||||
if profile_updated:
|
if profile_updated:
|
||||||
user.updated_at = datetime.utcnow()
|
user.updated_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(user)
|
await db.refresh(user)
|
||||||
logger.info('💾 Профиль пользователя обновлен', telegram_id=user.telegram_id)
|
logger.info('💾 Профиль пользователя обновлен', telegram_id=user.telegram_id)
|
||||||
@@ -589,8 +589,8 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession,
|
|||||||
user.username = message.from_user.username
|
user.username = message.from_user.username
|
||||||
user.first_name = message.from_user.first_name
|
user.first_name = message.from_user.first_name
|
||||||
user.last_name = message.from_user.last_name
|
user.last_name = message.from_user.last_name
|
||||||
user.updated_at = datetime.utcnow()
|
user.updated_at = datetime.now(UTC)
|
||||||
user.last_activity = datetime.utcnow()
|
user.last_activity = datetime.now(UTC)
|
||||||
|
|
||||||
from app.utils.user_utils import generate_unique_referral_code
|
from app.utils.user_utils import generate_unique_referral_code
|
||||||
|
|
||||||
@@ -1177,10 +1177,10 @@ async def complete_registration_from_callback(callback: types.CallbackQuery, sta
|
|||||||
existing_user.balance_kopeks = 0
|
existing_user.balance_kopeks = 0
|
||||||
existing_user.has_had_paid_subscription = False
|
existing_user.has_had_paid_subscription = False
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
existing_user.updated_at = datetime.utcnow()
|
existing_user.updated_at = datetime.now(UTC)
|
||||||
existing_user.last_activity = datetime.utcnow()
|
existing_user.last_activity = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(existing_user, ['subscription'])
|
await db.refresh(existing_user, ['subscription'])
|
||||||
@@ -1211,10 +1211,10 @@ async def complete_registration_from_callback(callback: types.CallbackQuery, sta
|
|||||||
if referrer_id and not existing_user.referred_by_id:
|
if referrer_id and not existing_user.referred_by_id:
|
||||||
existing_user.referred_by_id = referrer_id
|
existing_user.referred_by_id = referrer_id
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
existing_user.updated_at = datetime.utcnow()
|
existing_user.updated_at = datetime.now(UTC)
|
||||||
existing_user.last_activity = datetime.utcnow()
|
existing_user.last_activity = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(existing_user, ['subscription'])
|
await db.refresh(existing_user, ['subscription'])
|
||||||
@@ -1443,10 +1443,10 @@ async def complete_registration(message: types.Message, state: FSMContext, db: A
|
|||||||
existing_user.balance_kopeks = 0
|
existing_user.balance_kopeks = 0
|
||||||
existing_user.has_had_paid_subscription = False
|
existing_user.has_had_paid_subscription = False
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
existing_user.updated_at = datetime.utcnow()
|
existing_user.updated_at = datetime.now(UTC)
|
||||||
existing_user.last_activity = datetime.utcnow()
|
existing_user.last_activity = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(existing_user, ['subscription'])
|
await db.refresh(existing_user, ['subscription'])
|
||||||
@@ -1477,10 +1477,10 @@ async def complete_registration(message: types.Message, state: FSMContext, db: A
|
|||||||
if referrer_id and not existing_user.referred_by_id:
|
if referrer_id and not existing_user.referred_by_id:
|
||||||
existing_user.referred_by_id = referrer_id
|
existing_user.referred_by_id = referrer_id
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
existing_user.updated_at = datetime.utcnow()
|
existing_user.updated_at = datetime.now(UTC)
|
||||||
existing_user.last_activity = datetime.utcnow()
|
existing_user.last_activity = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(existing_user, ['subscription'])
|
await db.refresh(existing_user, ['subscription'])
|
||||||
@@ -1661,11 +1661,11 @@ def _get_subscription_status(user, texts):
|
|||||||
subscription = user.subscription
|
subscription = user.subscription
|
||||||
actual_status = getattr(subscription, 'actual_status', None)
|
actual_status = getattr(subscription, 'actual_status', None)
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
end_date = getattr(subscription, 'end_date', None)
|
end_date = getattr(subscription, 'end_date', None)
|
||||||
end_date_display = format_local_datetime(end_date, '%d.%m.%Y') if end_date else None
|
end_date_display = format_local_datetime(end_date, '%d.%m.%Y') if end_date else None
|
||||||
current_time = datetime.utcnow()
|
current_time = datetime.now(UTC)
|
||||||
|
|
||||||
if actual_status == 'disabled':
|
if actual_status == 'disabled':
|
||||||
return texts.t('SUB_STATUS_DISABLED', '⚫ Отключена')
|
return texts.t('SUB_STATUS_DISABLED', '⚫ Отключена')
|
||||||
@@ -1927,7 +1927,7 @@ async def required_sub_channel_check(
|
|||||||
subscription = user.subscription
|
subscription = user.subscription
|
||||||
if subscription.is_trial and subscription.status == SubscriptionStatus.DISABLED.value:
|
if subscription.is_trial and subscription.status == SubscriptionStatus.DISABLED.value:
|
||||||
subscription.status = SubscriptionStatus.ACTIVE.value
|
subscription.status = SubscriptionStatus.ACTIVE.value
|
||||||
subscription.updated_at = datetime.utcnow()
|
subscription.updated_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(subscription)
|
await db.refresh(subscription)
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
from aiogram import types
|
from aiogram import types
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
@@ -383,7 +383,7 @@ async def apply_countries_changes(callback: types.CallbackQuery, db_user: User,
|
|||||||
)
|
)
|
||||||
|
|
||||||
subscription.connected_squads = selected_countries
|
subscription.connected_squads = selected_countries
|
||||||
subscription.updated_at = datetime.utcnow()
|
subscription.updated_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
subscription_service = SubscriptionService()
|
subscription_service = SubscriptionService()
|
||||||
@@ -900,7 +900,7 @@ async def confirm_add_countries_to_subscription(
|
|||||||
)
|
)
|
||||||
|
|
||||||
subscription.connected_squads = selected_countries
|
subscription.connected_squads = selected_countries
|
||||||
subscription.updated_at = datetime.utcnow()
|
subscription.updated_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
subscription_service = SubscriptionService()
|
subscription_service = SubscriptionService()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
from aiogram import types
|
from aiogram import types
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
@@ -320,7 +320,7 @@ async def confirm_change_devices(callback: types.CallbackQuery, db_user: User, d
|
|||||||
|
|
||||||
if is_daily_tariff:
|
if is_daily_tariff:
|
||||||
# Для суточных тарифов считаем по дням (как в кабинете)
|
# Для суточных тарифов считаем по дням (как в кабинете)
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
days_left = max(1, (subscription.end_date - now).days)
|
days_left = max(1, (subscription.end_date - now).days)
|
||||||
period_hint_days = days_left
|
period_hint_days = days_left
|
||||||
|
|
||||||
@@ -555,7 +555,7 @@ async def execute_change_devices(callback: types.CallbackQuery, db_user: User, d
|
|||||||
)
|
)
|
||||||
|
|
||||||
subscription.device_limit = new_devices_count
|
subscription.device_limit = new_devices_count
|
||||||
subscription.updated_at = datetime.utcnow()
|
subscription.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
@@ -1093,7 +1093,7 @@ async def confirm_add_devices(callback: types.CallbackQuery, db_user: User, db:
|
|||||||
|
|
||||||
if is_daily_tariff:
|
if is_daily_tariff:
|
||||||
# Для суточных тарифов считаем по дням (как в кабинете)
|
# Для суточных тарифов считаем по дням (как в кабинете)
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
days_left = max(1, (subscription.end_date - now).days)
|
days_left = max(1, (subscription.end_date - now).days)
|
||||||
period_hint_days = days_left
|
period_hint_days = days_left
|
||||||
|
|
||||||
|
|||||||
@@ -446,13 +446,13 @@ async def get_subscription_info_text(subscription, texts, db_user, db: AsyncSess
|
|||||||
|
|
||||||
# Отображаем докупленный трафик
|
# Отображаем докупленный трафик
|
||||||
if (subscription.traffic_limit_gb or 0) > 0: # Только для лимитированных тарифов
|
if (subscription.traffic_limit_gb or 0) > 0: # Только для лимитированных тарифов
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
from sqlalchemy import select as sql_select
|
from sqlalchemy import select as sql_select
|
||||||
|
|
||||||
from app.database.models import TrafficPurchase
|
from app.database.models import TrafficPurchase
|
||||||
|
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
purchases_query = (
|
purchases_query = (
|
||||||
sql_select(TrafficPurchase)
|
sql_select(TrafficPurchase)
|
||||||
.where(TrafficPurchase.subscription_id == subscription.id)
|
.where(TrafficPurchase.subscription_id == subscription.id)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from aiogram import types
|
from aiogram import types
|
||||||
@@ -126,7 +126,7 @@ async def claim_discount_offer(
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
if offer.claimed_at is not None:
|
if offer.claimed_at is not None:
|
||||||
await callback.answer(
|
await callback.answer(
|
||||||
texts.get('DISCOUNT_CLAIM_ALREADY', 'ℹ️ Скидка уже была активирована'),
|
texts.get('DISCOUNT_CLAIM_ALREADY', 'ℹ️ Скидка уже была активирована'),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
@@ -206,7 +206,7 @@ async def show_subscription_info(callback: types.CallbackQuery, db_user: User, d
|
|||||||
await db.refresh(subscription)
|
await db.refresh(subscription)
|
||||||
await db.refresh(db_user)
|
await db.refresh(db_user)
|
||||||
|
|
||||||
current_time = datetime.utcnow()
|
current_time = datetime.now(UTC)
|
||||||
|
|
||||||
if subscription.status == 'expired' or subscription.end_date <= current_time:
|
if subscription.status == 'expired' or subscription.end_date <= current_time:
|
||||||
actual_status = 'expired'
|
actual_status = 'expired'
|
||||||
@@ -348,10 +348,10 @@ async def show_subscription_info(callback: types.CallbackQuery, db_user: User, d
|
|||||||
tariff_info_lines.append('⏸️ <b>Подписка приостановлена</b>')
|
tariff_info_lines.append('⏸️ <b>Подписка приостановлена</b>')
|
||||||
# Показываем оставшееся время даже при паузе
|
# Показываем оставшееся время даже при паузе
|
||||||
if last_charge:
|
if last_charge:
|
||||||
from datetime import timedelta
|
from datetime import UTC, timedelta
|
||||||
|
|
||||||
next_charge = last_charge + timedelta(hours=24)
|
next_charge = last_charge + timedelta(hours=24)
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
if next_charge > now:
|
if next_charge > now:
|
||||||
time_until = next_charge - now
|
time_until = next_charge - now
|
||||||
hours_left = time_until.seconds // 3600
|
hours_left = time_until.seconds // 3600
|
||||||
@@ -359,10 +359,10 @@ async def show_subscription_info(callback: types.CallbackQuery, db_user: User, d
|
|||||||
tariff_info_lines.append(f'⏳ Осталось: {hours_left}ч {minutes_left}мин')
|
tariff_info_lines.append(f'⏳ Осталось: {hours_left}ч {minutes_left}мин')
|
||||||
tariff_info_lines.append('💤 Списание приостановлено')
|
tariff_info_lines.append('💤 Списание приостановлено')
|
||||||
elif last_charge:
|
elif last_charge:
|
||||||
from datetime import timedelta
|
from datetime import UTC, timedelta
|
||||||
|
|
||||||
next_charge = last_charge + timedelta(hours=24)
|
next_charge = last_charge + timedelta(hours=24)
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
|
|
||||||
if next_charge > now:
|
if next_charge > now:
|
||||||
time_until = next_charge - now
|
time_until = next_charge - now
|
||||||
@@ -470,7 +470,7 @@ async def show_subscription_info(callback: types.CallbackQuery, db_user: User, d
|
|||||||
|
|
||||||
from app.database.models import TrafficPurchase
|
from app.database.models import TrafficPurchase
|
||||||
|
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
purchases_query = (
|
purchases_query = (
|
||||||
sql_select(TrafficPurchase)
|
sql_select(TrafficPurchase)
|
||||||
.where(TrafficPurchase.subscription_id == subscription.id)
|
.where(TrafficPurchase.subscription_id == subscription.id)
|
||||||
@@ -1970,7 +1970,7 @@ async def confirm_extend_subscription(callback: types.CallbackQuery, db_user: Us
|
|||||||
await callback.answer('⚠ Ошибка списания средств', show_alert=True)
|
await callback.answer('⚠ Ошибка списания средств', show_alert=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
current_time = datetime.utcnow()
|
current_time = datetime.now(UTC)
|
||||||
|
|
||||||
if subscription.end_date > current_time:
|
if subscription.end_date > current_time:
|
||||||
new_end_date = subscription.end_date + timedelta(days=days)
|
new_end_date = subscription.end_date + timedelta(days=days)
|
||||||
@@ -2589,7 +2589,7 @@ async def confirm_purchase(callback: types.CallbackQuery, state: FSMContext, db_
|
|||||||
should_update_devices = selected_devices is not None
|
should_update_devices = selected_devices is not None
|
||||||
|
|
||||||
was_trial_conversion = False
|
was_trial_conversion = False
|
||||||
current_time = datetime.utcnow()
|
current_time = datetime.now(UTC)
|
||||||
|
|
||||||
if existing_subscription:
|
if existing_subscription:
|
||||||
logger.info('Обновляем существующую подписку пользователя', telegram_id=db_user.telegram_id)
|
logger.info('Обновляем существующую подписку пользователя', telegram_id=db_user.telegram_id)
|
||||||
@@ -4311,7 +4311,7 @@ async def _extend_existing_subscription(
|
|||||||
squad_uuid: str,
|
squad_uuid: str,
|
||||||
):
|
):
|
||||||
"""Продлевает существующую подписку."""
|
"""Продлевает существующую подписку."""
|
||||||
from datetime import datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
from app.database.crud.transaction import create_transaction
|
from app.database.crud.transaction import create_transaction
|
||||||
from app.database.crud.user import subtract_user_balance
|
from app.database.crud.user import subtract_user_balance
|
||||||
@@ -4408,7 +4408,7 @@ async def _extend_existing_subscription(
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Обновляем параметры подписки
|
# Обновляем параметры подписки
|
||||||
current_time = datetime.utcnow()
|
current_time = datetime.now(UTC)
|
||||||
old_end_date = current_subscription.end_date
|
old_end_date = current_subscription.end_date
|
||||||
|
|
||||||
# Обновляем параметры в зависимости от типа текущей подписки
|
# Обновляем параметры в зависимости от типа текущей подписки
|
||||||
|
|||||||
@@ -1251,7 +1251,7 @@ async def confirm_daily_tariff_purchase(
|
|||||||
state: FSMContext,
|
state: FSMContext,
|
||||||
):
|
):
|
||||||
"""Подтверждает покупку суточного тарифа."""
|
"""Подтверждает покупку суточного тарифа."""
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
tariff_id = int(callback.data.split(':')[1])
|
tariff_id = int(callback.data.split(':')[1])
|
||||||
tariff = await get_tariff_by_id(db, tariff_id)
|
tariff = await get_tariff_by_id(db, tariff_id)
|
||||||
@@ -1314,9 +1314,9 @@ async def confirm_daily_tariff_purchase(
|
|||||||
existing_subscription.status = 'active'
|
existing_subscription.status = 'active'
|
||||||
existing_subscription.is_trial = False # Сбрасываем триальный статус
|
existing_subscription.is_trial = False # Сбрасываем триальный статус
|
||||||
existing_subscription.is_daily_paused = False
|
existing_subscription.is_daily_paused = False
|
||||||
existing_subscription.last_daily_charge_at = datetime.utcnow()
|
existing_subscription.last_daily_charge_at = datetime.now(UTC)
|
||||||
# Для суточного тарифа ставим срок на 1 день
|
# Для суточного тарифа ставим срок на 1 день
|
||||||
existing_subscription.end_date = datetime.utcnow() + timedelta(days=1)
|
existing_subscription.end_date = datetime.now(UTC) + timedelta(days=1)
|
||||||
|
|
||||||
# Сбрасываем докупленный трафик при смене тарифа
|
# Сбрасываем докупленный трафик при смене тарифа
|
||||||
from sqlalchemy import delete as sql_delete
|
from sqlalchemy import delete as sql_delete
|
||||||
@@ -1344,7 +1344,7 @@ async def confirm_daily_tariff_purchase(
|
|||||||
tariff_id=tariff.id,
|
tariff_id=tariff.id,
|
||||||
)
|
)
|
||||||
# Устанавливаем время последнего списания
|
# Устанавливаем время последнего списания
|
||||||
subscription.last_daily_charge_at = datetime.utcnow()
|
subscription.last_daily_charge_at = datetime.now(UTC)
|
||||||
subscription.is_daily_paused = False
|
subscription.is_daily_paused = False
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(subscription)
|
await db.refresh(subscription)
|
||||||
@@ -2022,9 +2022,9 @@ async def select_tariff_switch(
|
|||||||
current_subscription = await get_subscription_by_user_id(db, db_user.id)
|
current_subscription = await get_subscription_by_user_id(db, db_user.id)
|
||||||
days_warning = ''
|
days_warning = ''
|
||||||
if current_subscription and current_subscription.end_date:
|
if current_subscription and current_subscription.end_date:
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
remaining = current_subscription.end_date - datetime.utcnow()
|
remaining = current_subscription.end_date - datetime.now(UTC)
|
||||||
remaining_days = max(0, remaining.days)
|
remaining_days = max(0, remaining.days)
|
||||||
if remaining_days > 1:
|
if remaining_days > 1:
|
||||||
days_warning = f'\n\n⚠️ <b>Внимание!</b> У вас осталось {remaining_days} дн. подписки.\nПри смене на суточный тариф они будут утеряны!'
|
days_warning = f'\n\n⚠️ <b>Внимание!</b> У вас осталось {remaining_days} дн. подписки.\nПри смене на суточный тариф они будут утеряны!'
|
||||||
@@ -2103,7 +2103,7 @@ async def select_tariff_switch_period(
|
|||||||
state: FSMContext,
|
state: FSMContext,
|
||||||
):
|
):
|
||||||
"""Обрабатывает выбор периода для переключения тарифа."""
|
"""Обрабатывает выбор периода для переключения тарифа."""
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
parts = callback.data.split(':')
|
parts = callback.data.split(':')
|
||||||
tariff_id = int(parts[1])
|
tariff_id = int(parts[1])
|
||||||
@@ -2140,7 +2140,7 @@ async def select_tariff_switch_period(
|
|||||||
# Получаем текущую подписку для расчёта оставшегося времени
|
# Получаем текущую подписку для расчёта оставшегося времени
|
||||||
subscription = await get_subscription_by_user_id(db, db_user.id)
|
subscription = await get_subscription_by_user_id(db, db_user.id)
|
||||||
if subscription and subscription.end_date:
|
if subscription and subscription.end_date:
|
||||||
max(0, (subscription.end_date - datetime.utcnow()).days)
|
max(0, (subscription.end_date - datetime.now(UTC)).days)
|
||||||
|
|
||||||
# При смене тарифа устанавливается ровно оплаченный период
|
# При смене тарифа устанавливается ровно оплаченный период
|
||||||
time_info = f'⏰ Будет установлено: {period} дней'
|
time_info = f'⏰ Будет установлено: {period} дней'
|
||||||
@@ -2361,7 +2361,7 @@ async def confirm_daily_tariff_switch(
|
|||||||
state: FSMContext,
|
state: FSMContext,
|
||||||
):
|
):
|
||||||
"""Подтверждает смену на суточный тариф."""
|
"""Подтверждает смену на суточный тариф."""
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
tariff_id = int(callback.data.split(':')[1])
|
tariff_id = int(callback.data.split(':')[1])
|
||||||
tariff = await get_tariff_by_id(db, tariff_id)
|
tariff = await get_tariff_by_id(db, tariff_id)
|
||||||
@@ -2426,9 +2426,9 @@ async def confirm_daily_tariff_switch(
|
|||||||
subscription.status = 'active'
|
subscription.status = 'active'
|
||||||
subscription.is_trial = False # Сбрасываем триальный статус
|
subscription.is_trial = False # Сбрасываем триальный статус
|
||||||
subscription.is_daily_paused = False
|
subscription.is_daily_paused = False
|
||||||
subscription.last_daily_charge_at = datetime.utcnow()
|
subscription.last_daily_charge_at = datetime.now(UTC)
|
||||||
# Для суточного тарифа ставим срок на 1 день
|
# Для суточного тарифа ставим срок на 1 день
|
||||||
subscription.end_date = datetime.utcnow() + timedelta(days=1)
|
subscription.end_date = datetime.now(UTC) + timedelta(days=1)
|
||||||
|
|
||||||
# Сбрасываем докупленный трафик при смене тарифа
|
# Сбрасываем докупленный трафик при смене тарифа
|
||||||
from sqlalchemy import delete as sql_delete
|
from sqlalchemy import delete as sql_delete
|
||||||
@@ -2686,7 +2686,7 @@ async def show_instant_switch_list(
|
|||||||
state: FSMContext,
|
state: FSMContext,
|
||||||
):
|
):
|
||||||
"""Показывает список тарифов для мгновенного переключения."""
|
"""Показывает список тарифов для мгновенного переключения."""
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
texts = get_texts(db_user.language)
|
texts = get_texts(db_user.language)
|
||||||
await state.clear()
|
await state.clear()
|
||||||
@@ -2710,7 +2710,7 @@ async def show_instant_switch_list(
|
|||||||
# Рассчитываем оставшиеся дни
|
# Рассчитываем оставшиеся дни
|
||||||
remaining_days = 0
|
remaining_days = 0
|
||||||
if subscription.end_date:
|
if subscription.end_date:
|
||||||
remaining_days = max(0, (subscription.end_date - datetime.utcnow()).days)
|
remaining_days = max(0, (subscription.end_date - datetime.now(UTC)).days)
|
||||||
|
|
||||||
if remaining_days == 0:
|
if remaining_days == 0:
|
||||||
await callback.message.edit_text(
|
await callback.message.edit_text(
|
||||||
@@ -2767,7 +2767,7 @@ async def preview_instant_switch(
|
|||||||
state: FSMContext,
|
state: FSMContext,
|
||||||
):
|
):
|
||||||
"""Показывает превью мгновенного переключения тарифа."""
|
"""Показывает превью мгновенного переключения тарифа."""
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
tariff_id = int(callback.data.split(':')[1])
|
tariff_id = int(callback.data.split(':')[1])
|
||||||
new_tariff = await get_tariff_by_id(db, tariff_id)
|
new_tariff = await get_tariff_by_id(db, tariff_id)
|
||||||
@@ -2794,7 +2794,7 @@ async def preview_instant_switch(
|
|||||||
return
|
return
|
||||||
|
|
||||||
if not remaining_days and subscription.end_date:
|
if not remaining_days and subscription.end_date:
|
||||||
remaining_days = max(0, (subscription.end_date - datetime.utcnow()).days)
|
remaining_days = max(0, (subscription.end_date - datetime.now(UTC)).days)
|
||||||
|
|
||||||
# Рассчитываем стоимость переключения
|
# Рассчитываем стоимость переключения
|
||||||
upgrade_cost, is_upgrade = _calculate_instant_switch_cost(current_tariff, new_tariff, remaining_days, db_user)
|
upgrade_cost, is_upgrade = _calculate_instant_switch_cost(current_tariff, new_tariff, remaining_days, db_user)
|
||||||
@@ -2925,7 +2925,7 @@ async def confirm_instant_switch(
|
|||||||
state: FSMContext,
|
state: FSMContext,
|
||||||
):
|
):
|
||||||
"""Подтверждает мгновенное переключение тарифа."""
|
"""Подтверждает мгновенное переключение тарифа."""
|
||||||
from datetime import datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
tariff_id = int(callback.data.split(':')[1])
|
tariff_id = int(callback.data.split(':')[1])
|
||||||
new_tariff = await get_tariff_by_id(db, tariff_id)
|
new_tariff = await get_tariff_by_id(db, tariff_id)
|
||||||
@@ -3013,10 +3013,10 @@ async def confirm_instant_switch(
|
|||||||
description=f'Переключение на суточный тариф {new_tariff.name} (первый день)',
|
description=f'Переключение на суточный тариф {new_tariff.name} (первый день)',
|
||||||
)
|
)
|
||||||
|
|
||||||
subscription.end_date = datetime.utcnow() + timedelta(days=1)
|
subscription.end_date = datetime.now(UTC) + timedelta(days=1)
|
||||||
subscription.is_trial = False
|
subscription.is_trial = False
|
||||||
subscription.is_daily_paused = False
|
subscription.is_daily_paused = False
|
||||||
subscription.last_daily_charge_at = datetime.utcnow()
|
subscription.last_daily_charge_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(subscription)
|
await db.refresh(subscription)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
from aiogram import types
|
from aiogram import types
|
||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
@@ -303,7 +303,7 @@ async def confirm_reset_traffic(callback: types.CallbackQuery, db_user: User, db
|
|||||||
return
|
return
|
||||||
|
|
||||||
subscription.traffic_used_gb = 0.0
|
subscription.traffic_used_gb = 0.0
|
||||||
subscription.updated_at = datetime.utcnow()
|
subscription.updated_at = datetime.now(UTC)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
SubscriptionService()
|
SubscriptionService()
|
||||||
@@ -826,7 +826,7 @@ async def execute_switch_traffic(callback: types.CallbackQuery, db_user: User, d
|
|||||||
await db.execute(delete(TrafficPurchase).where(TrafficPurchase.subscription_id == subscription.id))
|
await db.execute(delete(TrafficPurchase).where(TrafficPurchase.subscription_id == subscription.id))
|
||||||
subscription.purchased_traffic_gb = 0
|
subscription.purchased_traffic_gb = 0
|
||||||
subscription.traffic_reset_at = None # Сбрасываем дату сброса трафика
|
subscription.traffic_reset_at = None # Сбрасываем дату сброса трафика
|
||||||
subscription.updated_at = datetime.utcnow()
|
subscription.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ async def get_main_menu_keyboard_async(
|
|||||||
subscription_days_left = subscription.days_left
|
subscription_days_left = subscription.days_left
|
||||||
elif hasattr(subscription, 'end_date') and subscription.end_date:
|
elif hasattr(subscription, 'end_date') and subscription.end_date:
|
||||||
# Fallback: вычисляем вручную, используя UTC
|
# Fallback: вычисляем вручную, используя UTC
|
||||||
now_utc = datetime.now(UTC).replace(tzinfo=None)
|
now_utc = datetime.now(UTC)
|
||||||
days_left = (subscription.end_date - now_utc).days
|
days_left = (subscription.end_date - now_utc).days
|
||||||
subscription_days_left = max(0, days_left)
|
subscription_days_left = max(0, days_left)
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ async def get_main_menu_keyboard_async(
|
|||||||
|
|
||||||
# Дни с регистрации
|
# Дни с регистрации
|
||||||
if hasattr(user, 'created_at') and user.created_at:
|
if hasattr(user, 'created_at') and user.created_at:
|
||||||
now_utc = datetime.now(UTC).replace(tzinfo=None)
|
now_utc = datetime.now(UTC)
|
||||||
registration_days = (now_utc - user.created_at).days
|
registration_days = (now_utc - user.created_at).days
|
||||||
|
|
||||||
# ID промо-группы
|
# ID промо-группы
|
||||||
@@ -2033,9 +2033,9 @@ def get_change_devices_keyboard(
|
|||||||
# Для суточных тарифов считаем по дням, для обычных - по месяцам
|
# Для суточных тарифов считаем по дням, для обычных - по месяцам
|
||||||
if is_daily_tariff and subscription_end_date:
|
if is_daily_tariff and subscription_end_date:
|
||||||
# Суточный тариф: цена за оставшиеся дни (обычно 1 день)
|
# Суточный тариф: цена за оставшиеся дни (обычно 1 день)
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
days_left = max(1, (subscription_end_date - now).days)
|
days_left = max(1, (subscription_end_date - now).days)
|
||||||
# Множитель = days_left / 30 (как в кабинете)
|
# Множитель = days_left / 30 (как в кабинете)
|
||||||
price_multiplier = days_left / 30
|
price_multiplier = days_left / 30
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
@@ -194,10 +194,10 @@ class AuthMiddleware(BaseMiddleware):
|
|||||||
)
|
)
|
||||||
profile_updated = True
|
profile_updated = True
|
||||||
|
|
||||||
db_user.last_activity = datetime.utcnow()
|
db_user.last_activity = datetime.now(UTC)
|
||||||
|
|
||||||
if profile_updated:
|
if profile_updated:
|
||||||
db_user.updated_at = datetime.utcnow()
|
db_user.updated_at = datetime.now(UTC)
|
||||||
logger.info('💾 [Middleware] Профиль пользователя обновлен в middleware', user_id=user.id)
|
logger.info('💾 [Middleware] Профиль пользователя обновлен в middleware', user_id=user.id)
|
||||||
|
|
||||||
if db_user.remnawave_uuid:
|
if db_user.remnawave_uuid:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import redis.asyncio as aioredis
|
import redis.asyncio as aioredis
|
||||||
@@ -404,7 +404,7 @@ class ChannelCheckerMiddleware(BaseMiddleware):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Проверяем что подписка ещё не истекла
|
# Проверяем что подписка ещё не истекла
|
||||||
if subscription.end_date and subscription.end_date <= datetime.utcnow():
|
if subscription.end_date and subscription.end_date <= datetime.now(UTC):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Реактивируем в БД
|
# Реактивируем в БД
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
from datetime import datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
@@ -37,7 +37,7 @@ class SubscriptionStatusMiddleware(BaseMiddleware):
|
|||||||
|
|
||||||
if db and user and user.subscription:
|
if db and user and user.subscription:
|
||||||
try:
|
try:
|
||||||
current_time = datetime.utcnow()
|
current_time = datetime.now(UTC)
|
||||||
subscription = user.subscription
|
subscription = user.subscription
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
@@ -275,7 +275,7 @@ class AdminNotificationService:
|
|||||||
transaction=None,
|
transaction=None,
|
||||||
amount_kopeks=charged_amount_kopeks,
|
amount_kopeks=charged_amount_kopeks,
|
||||||
message='Trial activation',
|
message='Trial activation',
|
||||||
occurred_at=datetime.utcnow(),
|
occurred_at=datetime.now(UTC),
|
||||||
extra={
|
extra={
|
||||||
'charged_amount_kopeks': charged_amount_kopeks,
|
'charged_amount_kopeks': charged_amount_kopeks,
|
||||||
'trial_duration_days': settings.TRIAL_DURATION_DAYS,
|
'trial_duration_days': settings.TRIAL_DURATION_DAYS,
|
||||||
@@ -356,7 +356,7 @@ class AdminNotificationService:
|
|||||||
message_lines.append(f'🔗 <b>Реферер:</b> {referrer_info}')
|
message_lines.append(f'🔗 <b>Реферер:</b> {referrer_info}')
|
||||||
|
|
||||||
message_lines.append('')
|
message_lines.append('')
|
||||||
message_lines.append(f'⏰ <i>{format_local_datetime(datetime.utcnow(), "%d.%m.%Y %H:%M:%S")}</i>')
|
message_lines.append(f'⏰ <i>{format_local_datetime(datetime.now(UTC), "%d.%m.%Y %H:%M:%S")}</i>')
|
||||||
|
|
||||||
return await self._send_message('\n'.join(message_lines))
|
return await self._send_message('\n'.join(message_lines))
|
||||||
|
|
||||||
@@ -403,7 +403,7 @@ class AdminNotificationService:
|
|||||||
transaction=transaction,
|
transaction=transaction,
|
||||||
amount_kopeks=total_amount,
|
amount_kopeks=total_amount,
|
||||||
message='Subscription purchase',
|
message='Subscription purchase',
|
||||||
occurred_at=(transaction.completed_at or transaction.created_at) if transaction else datetime.utcnow(),
|
occurred_at=(transaction.completed_at or transaction.created_at) if transaction else datetime.now(UTC),
|
||||||
extra={
|
extra={
|
||||||
'period_days': period_days,
|
'period_days': period_days,
|
||||||
'was_trial_conversion': was_trial_conversion,
|
'was_trial_conversion': was_trial_conversion,
|
||||||
@@ -482,7 +482,7 @@ class AdminNotificationService:
|
|||||||
message_lines.extend(
|
message_lines.extend(
|
||||||
[
|
[
|
||||||
'',
|
'',
|
||||||
f'<i>{format_local_datetime(datetime.utcnow(), "%d.%m.%Y %H:%M")}</i>',
|
f'<i>{format_local_datetime(datetime.now(UTC), "%d.%m.%Y %H:%M")}</i>',
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -503,7 +503,7 @@ class AdminNotificationService:
|
|||||||
repo = getattr(settings, 'VERSION_CHECK_REPO', 'fr1ngg/remnawave-bedolaga-telegram-bot')
|
repo = getattr(settings, 'VERSION_CHECK_REPO', 'fr1ngg/remnawave-bedolaga-telegram-bot')
|
||||||
release_url = f'https://github.com/{repo}/releases/tag/{latest_version.tag_name}'
|
release_url = f'https://github.com/{repo}/releases/tag/{latest_version.tag_name}'
|
||||||
repo_url = f'https://github.com/{repo}'
|
repo_url = f'https://github.com/{repo}'
|
||||||
timestamp = format_local_datetime(datetime.utcnow(), '%d.%m.%Y %H:%M:%S')
|
timestamp = format_local_datetime(datetime.now(UTC), '%d.%m.%Y %H:%M:%S')
|
||||||
|
|
||||||
if latest_version.prerelease:
|
if latest_version.prerelease:
|
||||||
header = '🧪 <b>Pre-release</b>'
|
header = '🧪 <b>Pre-release</b>'
|
||||||
@@ -568,7 +568,7 @@ class AdminNotificationService:
|
|||||||
🔄 Следующая попытка через час.
|
🔄 Следующая попытка через час.
|
||||||
⚙️ Проверьте доступность GitHub API и настройки сети.
|
⚙️ Проверьте доступность GitHub API и настройки сети.
|
||||||
|
|
||||||
⚙️ <i>Система автоматических обновлений • {format_local_datetime(datetime.utcnow(), '%d.%m.%Y %H:%M:%S')}</i>"""
|
⚙️ <i>Система автоматических обновлений • {format_local_datetime(datetime.now(UTC), '%d.%m.%Y %H:%M:%S')}</i>"""
|
||||||
|
|
||||||
return await self._send_message(message)
|
return await self._send_message(message)
|
||||||
|
|
||||||
@@ -590,7 +590,7 @@ class AdminNotificationService:
|
|||||||
payment_method = self._get_payment_method_display(transaction.payment_method)
|
payment_method = self._get_payment_method_display(transaction.payment_method)
|
||||||
balance_change = user.balance_kopeks - old_balance
|
balance_change = user.balance_kopeks - old_balance
|
||||||
subscription_status = self._get_subscription_status(subscription)
|
subscription_status = self._get_subscription_status(subscription)
|
||||||
timestamp = format_local_datetime(datetime.utcnow(), '%d.%m.%Y %H:%M:%S')
|
timestamp = format_local_datetime(datetime.now(UTC), '%d.%m.%Y %H:%M:%S')
|
||||||
user_display = self._get_user_display(user)
|
user_display = self._get_user_display(user)
|
||||||
user_id_display = self._get_user_identifier_display(user)
|
user_id_display = self._get_user_identifier_display(user)
|
||||||
|
|
||||||
@@ -883,7 +883,7 @@ class AdminNotificationService:
|
|||||||
|
|
||||||
💰 <b>Баланс после операции:</b> {settings.format_price(current_balance)}
|
💰 <b>Баланс после операции:</b> {settings.format_price(current_balance)}
|
||||||
|
|
||||||
⏰ <i>{format_local_datetime(datetime.utcnow(), '%d.%m.%Y %H:%M:%S')}</i>"""
|
⏰ <i>{format_local_datetime(datetime.now(UTC), '%d.%m.%Y %H:%M:%S')}</i>"""
|
||||||
|
|
||||||
return await self._send_message(message)
|
return await self._send_message(message)
|
||||||
|
|
||||||
@@ -909,7 +909,7 @@ class AdminNotificationService:
|
|||||||
transaction=None,
|
transaction=None,
|
||||||
amount_kopeks=promocode_data.get('balance_bonus_kopeks'),
|
amount_kopeks=promocode_data.get('balance_bonus_kopeks'),
|
||||||
message='Promocode activation',
|
message='Promocode activation',
|
||||||
occurred_at=datetime.utcnow(),
|
occurred_at=datetime.now(UTC),
|
||||||
extra={
|
extra={
|
||||||
'code': promocode_data.get('code'),
|
'code': promocode_data.get('code'),
|
||||||
'type': promocode_data.get('type'),
|
'type': promocode_data.get('type'),
|
||||||
@@ -988,7 +988,7 @@ class AdminNotificationService:
|
|||||||
'📝 <b>Эффект:</b>',
|
'📝 <b>Эффект:</b>',
|
||||||
effect_description.strip() or '✅ Промокод активирован',
|
effect_description.strip() or '✅ Промокод активирован',
|
||||||
'',
|
'',
|
||||||
f'⏰ <i>{format_local_datetime(datetime.utcnow(), "%d.%m.%Y %H:%M:%S")}</i>',
|
f'⏰ <i>{format_local_datetime(datetime.now(UTC), "%d.%m.%Y %H:%M:%S")}</i>',
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1015,7 +1015,7 @@ class AdminNotificationService:
|
|||||||
transaction=None,
|
transaction=None,
|
||||||
amount_kopeks=None,
|
amount_kopeks=None,
|
||||||
message='Referral link visit',
|
message='Referral link visit',
|
||||||
occurred_at=datetime.utcnow(),
|
occurred_at=datetime.now(UTC),
|
||||||
extra={
|
extra={
|
||||||
'campaign_id': campaign.id,
|
'campaign_id': campaign.id,
|
||||||
'campaign_name': campaign.name,
|
'campaign_name': campaign.name,
|
||||||
@@ -1067,7 +1067,7 @@ class AdminNotificationService:
|
|||||||
message_lines.extend(
|
message_lines.extend(
|
||||||
[
|
[
|
||||||
'',
|
'',
|
||||||
f'⏰ <i>{format_local_datetime(datetime.utcnow(), "%d.%m.%Y %H:%M:%S")}</i>',
|
f'⏰ <i>{format_local_datetime(datetime.now(UTC), "%d.%m.%Y %H:%M:%S")}</i>',
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1096,7 +1096,7 @@ class AdminNotificationService:
|
|||||||
subscription=None,
|
subscription=None,
|
||||||
transaction=None,
|
transaction=None,
|
||||||
message='Promo group change',
|
message='Promo group change',
|
||||||
occurred_at=datetime.utcnow(),
|
occurred_at=datetime.now(UTC),
|
||||||
extra={
|
extra={
|
||||||
'old_group_id': getattr(old_group, 'id', None),
|
'old_group_id': getattr(old_group, 'id', None),
|
||||||
'old_group_name': getattr(old_group, 'name', None),
|
'old_group_name': getattr(old_group, 'name', None),
|
||||||
@@ -1157,7 +1157,7 @@ class AdminNotificationService:
|
|||||||
[
|
[
|
||||||
'',
|
'',
|
||||||
f'💰 Баланс пользователя: {settings.format_price(user.balance_kopeks)}',
|
f'💰 Баланс пользователя: {settings.format_price(user.balance_kopeks)}',
|
||||||
f'⏰ <i>{format_local_datetime(datetime.utcnow(), "%d.%m.%Y %H:%M:%S")}</i>',
|
f'⏰ <i>{format_local_datetime(datetime.now(UTC), "%d.%m.%Y %H:%M:%S")}</i>',
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1325,7 +1325,7 @@ class AdminNotificationService:
|
|||||||
if details.get('enabled_at'):
|
if details.get('enabled_at'):
|
||||||
enabled_at = details['enabled_at']
|
enabled_at = details['enabled_at']
|
||||||
if isinstance(enabled_at, str):
|
if isinstance(enabled_at, str):
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
enabled_at = datetime.fromisoformat(enabled_at)
|
enabled_at = datetime.fromisoformat(enabled_at)
|
||||||
message_parts.append(
|
message_parts.append(
|
||||||
@@ -1342,7 +1342,7 @@ class AdminNotificationService:
|
|||||||
if details.get('disabled_at'):
|
if details.get('disabled_at'):
|
||||||
disabled_at = details['disabled_at']
|
disabled_at = details['disabled_at']
|
||||||
if isinstance(disabled_at, str):
|
if isinstance(disabled_at, str):
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
disabled_at = datetime.fromisoformat(disabled_at)
|
disabled_at = datetime.fromisoformat(disabled_at)
|
||||||
message_parts.append(
|
message_parts.append(
|
||||||
@@ -1409,7 +1409,7 @@ class AdminNotificationService:
|
|||||||
message_parts.append('Автоматический мониторинг API остановлен.')
|
message_parts.append('Автоматический мониторинг API остановлен.')
|
||||||
|
|
||||||
message_parts.append('')
|
message_parts.append('')
|
||||||
message_parts.append(f'⏰ <i>{format_local_datetime(datetime.utcnow(), "%d.%m.%Y %H:%M:%S")}</i>')
|
message_parts.append(f'⏰ <i>{format_local_datetime(datetime.now(UTC), "%d.%m.%Y %H:%M:%S")}</i>')
|
||||||
|
|
||||||
message = '\n'.join(message_parts)
|
message = '\n'.join(message_parts)
|
||||||
|
|
||||||
@@ -1446,7 +1446,7 @@ class AdminNotificationService:
|
|||||||
if details.get('last_check'):
|
if details.get('last_check'):
|
||||||
last_check = details['last_check']
|
last_check = details['last_check']
|
||||||
if isinstance(last_check, str):
|
if isinstance(last_check, str):
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
last_check = datetime.fromisoformat(last_check)
|
last_check = datetime.fromisoformat(last_check)
|
||||||
message_parts.append(f'🕐 <b>Последняя проверка:</b> {format_local_datetime(last_check, "%H:%M:%S")}')
|
message_parts.append(f'🕐 <b>Последняя проверка:</b> {format_local_datetime(last_check, "%H:%M:%S")}')
|
||||||
@@ -1496,7 +1496,7 @@ class AdminNotificationService:
|
|||||||
message_parts.append('Панель временно недоступна для обслуживания.')
|
message_parts.append('Панель временно недоступна для обслуживания.')
|
||||||
|
|
||||||
message_parts.append('')
|
message_parts.append('')
|
||||||
message_parts.append(f'⏰ <i>{format_local_datetime(datetime.utcnow(), "%d.%m.%Y %H:%M:%S")}</i>')
|
message_parts.append(f'⏰ <i>{format_local_datetime(datetime.now(UTC), "%d.%m.%Y %H:%M:%S")}</i>')
|
||||||
|
|
||||||
message = '\n'.join(message_parts)
|
message = '\n'.join(message_parts)
|
||||||
|
|
||||||
@@ -1589,7 +1589,7 @@ class AdminNotificationService:
|
|||||||
message_lines.extend(
|
message_lines.extend(
|
||||||
[
|
[
|
||||||
'',
|
'',
|
||||||
f'<i>{format_local_datetime(datetime.utcnow(), "%d.%m.%Y %H:%M")}</i>',
|
f'<i>{format_local_datetime(datetime.now(UTC), "%d.%m.%Y %H:%M")}</i>',
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1662,7 +1662,7 @@ class AdminNotificationService:
|
|||||||
message_lines.extend(
|
message_lines.extend(
|
||||||
[
|
[
|
||||||
'',
|
'',
|
||||||
f'⏰ <i>{format_local_datetime(datetime.utcnow(), "%d.%m.%Y %H:%M:%S")}</i>',
|
f'⏰ <i>{format_local_datetime(datetime.now(UTC), "%d.%m.%Y %H:%M:%S")}</i>',
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import shutil
|
|||||||
import tarfile
|
import tarfile
|
||||||
import tempfile
|
import tempfile
|
||||||
from dataclasses import asdict, dataclass
|
from dataclasses import asdict, dataclass
|
||||||
from datetime import date as dt_date, datetime, time as dt_time, timedelta
|
from datetime import UTC, date as dt_date, datetime, time as dt_time, timedelta
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -262,7 +262,7 @@ class BackupService:
|
|||||||
return default_hours, default_minutes
|
return default_hours, default_minutes
|
||||||
|
|
||||||
def _calculate_next_backup_datetime(self, reference: datetime | None = None) -> datetime:
|
def _calculate_next_backup_datetime(self, reference: datetime | None = None) -> datetime:
|
||||||
reference = reference or datetime.now()
|
reference = reference or datetime.now(UTC)
|
||||||
hours, minutes = self._parse_backup_time()
|
hours, minutes = self._parse_backup_time()
|
||||||
|
|
||||||
next_run = reference.replace(hour=hours, minute=minutes, second=0, microsecond=0)
|
next_run = reference.replace(hour=hours, minute=minutes, second=0, microsecond=0)
|
||||||
@@ -319,7 +319,7 @@ class BackupService:
|
|||||||
|
|
||||||
overview = await self._collect_database_overview()
|
overview = await self._collect_database_overview()
|
||||||
|
|
||||||
timestamp = datetime.utcnow().strftime('%Y%m%d_%H%M%S')
|
timestamp = datetime.now(UTC).strftime('%Y%m%d_%H%M%S')
|
||||||
archive_suffix = '.tar.gz' if compress else '.tar'
|
archive_suffix = '.tar.gz' if compress else '.tar'
|
||||||
filename = f'backup_{timestamp}{archive_suffix}'
|
filename = f'backup_{timestamp}{archive_suffix}'
|
||||||
backup_path = self.backup_dir / filename
|
backup_path = self.backup_dir / filename
|
||||||
@@ -337,7 +337,7 @@ class BackupService:
|
|||||||
|
|
||||||
metadata = {
|
metadata = {
|
||||||
'format_version': self.archive_format_version,
|
'format_version': self.archive_format_version,
|
||||||
'timestamp': datetime.utcnow().isoformat(),
|
'timestamp': datetime.now(UTC).isoformat(),
|
||||||
'database_type': 'postgresql' if settings.is_postgresql() else 'sqlite',
|
'database_type': 'postgresql' if settings.is_postgresql() else 'sqlite',
|
||||||
'backup_type': 'full',
|
'backup_type': 'full',
|
||||||
'tables_count': overview.get('tables_count', 0),
|
'tables_count': overview.get('tables_count', 0),
|
||||||
@@ -525,7 +525,7 @@ class BackupService:
|
|||||||
dump_path = staging_dir / 'database.json'
|
dump_path = staging_dir / 'database.json'
|
||||||
dump_structure = {
|
dump_structure = {
|
||||||
'metadata': {
|
'metadata': {
|
||||||
'timestamp': datetime.utcnow().isoformat(),
|
'timestamp': datetime.now(UTC).isoformat(),
|
||||||
'version': 'orm-1.0',
|
'version': 'orm-1.0',
|
||||||
'database_type': 'postgresql',
|
'database_type': 'postgresql',
|
||||||
'tables_count': tables_count,
|
'tables_count': tables_count,
|
||||||
@@ -1171,10 +1171,10 @@ class BackupService:
|
|||||||
if 'T' in value:
|
if 'T' in value:
|
||||||
processed_data[key] = datetime.fromisoformat(value.replace('Z', '+00:00'))
|
processed_data[key] = datetime.fromisoformat(value.replace('Z', '+00:00'))
|
||||||
else:
|
else:
|
||||||
processed_data[key] = datetime.strptime(value, '%Y-%m-%d %H:%M:%S')
|
processed_data[key] = datetime.strptime(value, '%Y-%m-%d %H:%M:%S').replace(tzinfo=UTC)
|
||||||
except (ValueError, TypeError) as e:
|
except (ValueError, TypeError) as e:
|
||||||
logger.warning('Не удалось парсить дату для поля', value=value, key=key, error=e)
|
logger.warning('Не удалось парсить дату для поля', value=value, key=key, error=e)
|
||||||
processed_data[key] = datetime.utcnow()
|
processed_data[key] = datetime.now(UTC)
|
||||||
elif column_type_str == 'TIME' and isinstance(value, str):
|
elif column_type_str == 'TIME' and isinstance(value, str):
|
||||||
try:
|
try:
|
||||||
processed_data[key] = dt_time.fromisoformat(value)
|
processed_data[key] = dt_time.fromisoformat(value)
|
||||||
@@ -1494,7 +1494,7 @@ class BackupService:
|
|||||||
snapshots['app_config'] = {
|
snapshots['app_config'] = {
|
||||||
'path': str(path_obj),
|
'path': str(path_obj),
|
||||||
'content': content,
|
'content': content,
|
||||||
'modified_at': datetime.fromtimestamp(path_obj.stat().st_mtime).isoformat(),
|
'modified_at': datetime.fromtimestamp(path_obj.stat().st_mtime, tz=UTC).isoformat(),
|
||||||
}
|
}
|
||||||
logger.info('📁 Добавлен в бекап файл конфигурации', path_obj=path_obj)
|
logger.info('📁 Добавлен в бекап файл конфигурации', path_obj=path_obj)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1557,7 +1557,9 @@ class BackupService:
|
|||||||
backup_info = {
|
backup_info = {
|
||||||
'filename': backup_file.name,
|
'filename': backup_file.name,
|
||||||
'filepath': str(backup_file),
|
'filepath': str(backup_file),
|
||||||
'timestamp': metadata.get('timestamp', datetime.fromtimestamp(file_stats.st_mtime).isoformat()),
|
'timestamp': metadata.get(
|
||||||
|
'timestamp', datetime.fromtimestamp(file_stats.st_mtime, tz=UTC).isoformat()
|
||||||
|
),
|
||||||
'tables_count': metadata.get(
|
'tables_count': metadata.get(
|
||||||
'tables_count', metadata.get('database', {}).get('tables_count', 0)
|
'tables_count', metadata.get('database', {}).get('tables_count', 0)
|
||||||
),
|
),
|
||||||
@@ -1583,7 +1585,7 @@ class BackupService:
|
|||||||
{
|
{
|
||||||
'filename': backup_file.name,
|
'filename': backup_file.name,
|
||||||
'filepath': str(backup_file),
|
'filepath': str(backup_file),
|
||||||
'timestamp': datetime.fromtimestamp(file_stats.st_mtime).isoformat(),
|
'timestamp': datetime.fromtimestamp(file_stats.st_mtime, tz=UTC).isoformat(),
|
||||||
'tables_count': '?',
|
'tables_count': '?',
|
||||||
'total_records': '?',
|
'total_records': '?',
|
||||||
'compressed': backup_file.suffix == '.gz',
|
'compressed': backup_file.suffix == '.gz',
|
||||||
@@ -1683,7 +1685,7 @@ class BackupService:
|
|||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
now = datetime.now()
|
now = datetime.now(UTC)
|
||||||
delay = (next_run - now).total_seconds()
|
delay = (next_run - now).total_seconds()
|
||||||
|
|
||||||
if delay > 0:
|
if delay > 0:
|
||||||
@@ -1713,7 +1715,7 @@ class BackupService:
|
|||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error('Ошибка в цикле автобекапов', error=e)
|
logger.error('Ошибка в цикле автобекапов', error=e)
|
||||||
next_run = datetime.now() + interval
|
next_run = datetime.now(UTC) + interval
|
||||||
|
|
||||||
async def _send_backup_notification(self, event_type: str, message: str, file_path: str = None):
|
async def _send_backup_notification(self, event_type: str, message: str, file_path: str = None):
|
||||||
try:
|
try:
|
||||||
@@ -1728,7 +1730,7 @@ class BackupService:
|
|||||||
if file_path:
|
if file_path:
|
||||||
notification_text += f'\n📁 <code>{Path(file_path).name}</code>'
|
notification_text += f'\n📁 <code>{Path(file_path).name}</code>'
|
||||||
|
|
||||||
notification_text += f'\n\n⏰ <i>{datetime.now().strftime("%d.%m.%Y %H:%M:%S")}</i>'
|
notification_text += f'\n\n⏰ <i>{datetime.now(UTC).strftime("%d.%m.%Y %H:%M:%S")}</i>'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from app.services.admin_notification_service import AdminNotificationService
|
from app.services.admin_notification_service import AdminNotificationService
|
||||||
@@ -1762,7 +1764,7 @@ class BackupService:
|
|||||||
caption = '📦 <b>Резервная копия</b>\n\n'
|
caption = '📦 <b>Резервная копия</b>\n\n'
|
||||||
if temp_zip_path:
|
if temp_zip_path:
|
||||||
caption += '🔐 <b>Архив защищён паролем</b>\n\n'
|
caption += '🔐 <b>Архив защищён паролем</b>\n\n'
|
||||||
caption += f'⏰ <i>{datetime.now().strftime("%d.%m.%Y %H:%M:%S")}</i>'
|
caption += f'⏰ <i>{datetime.now(UTC).strftime("%d.%m.%Y %H:%M:%S")}</i>'
|
||||||
|
|
||||||
send_kwargs = {
|
send_kwargs = {
|
||||||
'chat_id': chat_id,
|
'chat_id': chat_id,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import structlog
|
import structlog
|
||||||
@@ -122,7 +122,7 @@ class BlacklistService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.blacklist_data = blacklist_data
|
self.blacklist_data = blacklist_data
|
||||||
self.last_update = datetime.utcnow()
|
self.last_update = datetime.now(UTC)
|
||||||
self._check_cache.clear()
|
self._check_cache.clear()
|
||||||
logger.info('Черный список успешно обновлен. Найдено записей', blacklist_data_count=len(blacklist_data))
|
logger.info('Черный список успешно обновлен. Найдено записей', blacklist_data_count=len(blacklist_data))
|
||||||
return True
|
return True
|
||||||
@@ -164,7 +164,7 @@ class BlacklistService:
|
|||||||
# Если черный список пуст или устарел, обновляем его
|
# Если черный список пуст или устарел, обновляем его
|
||||||
interval_hours = self.get_blacklist_update_interval_hours()
|
interval_hours = self.get_blacklist_update_interval_hours()
|
||||||
required_interval = timedelta(hours=interval_hours)
|
required_interval = timedelta(hours=interval_hours)
|
||||||
if not self.blacklist_data or (self.last_update and datetime.utcnow() - self.last_update > required_interval):
|
if not self.blacklist_data or (self.last_update and datetime.now(UTC) - self.last_update > required_interval):
|
||||||
await self.update_blacklist()
|
await self.update_blacklist()
|
||||||
|
|
||||||
# Проверяем по Telegram ID
|
# Проверяем по Telegram ID
|
||||||
@@ -197,7 +197,7 @@ class BlacklistService:
|
|||||||
"""
|
"""
|
||||||
interval_hours = self.get_blacklist_update_interval_hours()
|
interval_hours = self.get_blacklist_update_interval_hours()
|
||||||
required_interval = timedelta(hours=interval_hours)
|
required_interval = timedelta(hours=interval_hours)
|
||||||
if not self.blacklist_data or (self.last_update and datetime.utcnow() - self.last_update > required_interval):
|
if not self.blacklist_data or (self.last_update and datetime.now(UTC) - self.last_update > required_interval):
|
||||||
await self.update_blacklist()
|
await self.update_blacklist()
|
||||||
|
|
||||||
return self.blacklist_data.copy()
|
return self.blacklist_data.copy()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
@@ -467,7 +467,7 @@ class BroadcastService:
|
|||||||
broadcast.status = status
|
broadcast.status = status
|
||||||
|
|
||||||
if update_completed_at:
|
if update_completed_at:
|
||||||
broadcast.completed_at = datetime.utcnow()
|
broadcast.completed_at = datetime.now(UTC)
|
||||||
|
|
||||||
await session.commit()
|
await session.commit()
|
||||||
return
|
return
|
||||||
@@ -865,7 +865,7 @@ class EmailBroadcastService:
|
|||||||
broadcast.status = status
|
broadcast.status = status
|
||||||
|
|
||||||
if update_completed_at:
|
if update_completed_at:
|
||||||
broadcast.completed_at = datetime.utcnow()
|
broadcast.completed_at = datetime.now(UTC)
|
||||||
|
|
||||||
await session.commit()
|
await session.commit()
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -221,8 +221,8 @@ class ContestRotationService:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Convert to UTC for storage
|
# Convert to UTC for storage
|
||||||
starts_at_utc = starts_at_local.astimezone(UTC).replace(tzinfo=None)
|
starts_at_utc = starts_at_local.astimezone(UTC)
|
||||||
ends_at_utc = ends_at_local.astimezone(UTC).replace(tzinfo=None)
|
ends_at_utc = ends_at_local.astimezone(UTC)
|
||||||
|
|
||||||
# Анонс перед созданием раунда
|
# Анонс перед созданием раунда
|
||||||
await self._announce_round_start(tpl, starts_at_local, ends_at_local)
|
await self._announce_round_start(tpl, starts_at_local, ends_at_local)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from aiogram import Bot
|
from aiogram import Bot
|
||||||
@@ -290,7 +290,7 @@ class DailySubscriptionService:
|
|||||||
async with AsyncSessionLocal() as db:
|
async with AsyncSessionLocal() as db:
|
||||||
try:
|
try:
|
||||||
# Находим все истекшие докупки
|
# Находим все истекшие докупки
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
query = select(TrafficPurchase).where(TrafficPurchase.expires_at <= now)
|
query = select(TrafficPurchase).where(TrafficPurchase.expires_at <= now)
|
||||||
result = await db.execute(query)
|
result = await db.execute(query)
|
||||||
expired_purchases = result.scalars().all()
|
expired_purchases = result.scalars().all()
|
||||||
@@ -400,7 +400,7 @@ class DailySubscriptionService:
|
|||||||
subscription.purchased_traffic_gb = max(0, new_purchased)
|
subscription.purchased_traffic_gb = max(0, new_purchased)
|
||||||
|
|
||||||
# Проверяем, остались ли активные докупки
|
# Проверяем, остались ли активные докупки
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
remaining_query = (
|
remaining_query = (
|
||||||
select(TrafficPurchase)
|
select(TrafficPurchase)
|
||||||
.where(TrafficPurchase.subscription_id == subscription_id)
|
.where(TrafficPurchase.subscription_id == subscription_id)
|
||||||
@@ -417,7 +417,7 @@ class DailySubscriptionService:
|
|||||||
next_expiry = min(p.expires_at for p in remaining_purchases)
|
next_expiry = min(p.expires_at for p in remaining_purchases)
|
||||||
subscription.traffic_reset_at = next_expiry
|
subscription.traffic_reset_at = next_expiry
|
||||||
|
|
||||||
subscription.updated_at = datetime.utcnow()
|
subscription.updated_at = datetime.now(UTC)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
@@ -60,7 +60,7 @@ class EventEmitter:
|
|||||||
event_data = {
|
event_data = {
|
||||||
'type': event_type,
|
'type': event_type,
|
||||||
'payload': payload,
|
'payload': payload,
|
||||||
'timestamp': str(datetime.utcnow()),
|
'timestamp': str(datetime.now(UTC)),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Вызываем локальные слушатели
|
# Вызываем локальные слушатели
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
@@ -76,7 +76,7 @@ class MaintenanceService:
|
|||||||
emoji_map = {'error': '🚨', 'warning': '⚠️', 'success': '✅', 'info': 'ℹ️'}
|
emoji_map = {'error': '🚨', 'warning': '⚠️', 'success': '✅', 'info': 'ℹ️'}
|
||||||
emoji = emoji_map.get(alert_type, 'ℹ️')
|
emoji = emoji_map.get(alert_type, 'ℹ️')
|
||||||
|
|
||||||
timestamp = format_local_datetime(datetime.utcnow(), '%d.%m.%Y %H:%M:%S %Z')
|
timestamp = format_local_datetime(datetime.now(UTC), '%d.%m.%Y %H:%M:%S %Z')
|
||||||
formatted_message = f'{emoji} <b>ТЕХНИЧЕСКИЕ РАБОТЫ</b>\n\n{message}\n\n⏰ <i>{timestamp}</i>'
|
formatted_message = f'{emoji} <b>ТЕХНИЧЕСКИЕ РАБОТЫ</b>\n\n{message}\n\n⏰ <i>{timestamp}</i>'
|
||||||
|
|
||||||
return await notification_service._send_message(formatted_message)
|
return await notification_service._send_message(formatted_message)
|
||||||
@@ -135,7 +135,7 @@ class MaintenanceService:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
self._status.is_active = True
|
self._status.is_active = True
|
||||||
self._status.enabled_at = datetime.utcnow()
|
self._status.enabled_at = datetime.now(UTC)
|
||||||
self._status.reason = reason or ('Автоматическое включение' if auto else 'Включено администратором')
|
self._status.reason = reason or ('Автоматическое включение' if auto else 'Включено администратором')
|
||||||
self._status.auto_enabled = auto
|
self._status.auto_enabled = auto
|
||||||
|
|
||||||
@@ -168,7 +168,7 @@ class MaintenanceService:
|
|||||||
was_auto = self._status.auto_enabled
|
was_auto = self._status.auto_enabled
|
||||||
duration = None
|
duration = None
|
||||||
if self._status.enabled_at:
|
if self._status.enabled_at:
|
||||||
duration = datetime.utcnow() - self._status.enabled_at
|
duration = datetime.now(UTC) - self._status.enabled_at
|
||||||
|
|
||||||
self._status.is_active = False
|
self._status.is_active = False
|
||||||
self._status.enabled_at = None
|
self._status.enabled_at = None
|
||||||
@@ -187,7 +187,7 @@ class MaintenanceService:
|
|||||||
else:
|
else:
|
||||||
duration_str = f'\n⏱️ <b>Длительность:</b> {minutes}мин'
|
duration_str = f'\n⏱️ <b>Длительность:</b> {minutes}мин'
|
||||||
|
|
||||||
notification_time = format_local_datetime(datetime.utcnow(), '%d.%m.%Y %H:%M:%S %Z')
|
notification_time = format_local_datetime(datetime.now(UTC), '%d.%m.%Y %H:%M:%S %Z')
|
||||||
notification_msg = f"""Режим технических работ ВЫКЛЮЧЕН
|
notification_msg = f"""Режим технических работ ВЫКЛЮЧЕН
|
||||||
|
|
||||||
🤖 <b>Автоматически:</b> {'Да' if was_auto else 'Нет'}
|
🤖 <b>Автоматически:</b> {'Да' if was_auto else 'Нет'}
|
||||||
@@ -252,7 +252,7 @@ class MaintenanceService:
|
|||||||
return self._status.api_status
|
return self._status.api_status
|
||||||
|
|
||||||
self._is_checking = True
|
self._is_checking = True
|
||||||
self._status.last_check = datetime.utcnow()
|
self._status.last_check = datetime.now(UTC)
|
||||||
|
|
||||||
auth_params = settings.get_remnawave_auth_params()
|
auth_params = settings.get_remnawave_auth_params()
|
||||||
base_url = (auth_params.get('base_url') or '').strip()
|
base_url = (auth_params.get('base_url') or '').strip()
|
||||||
@@ -356,7 +356,7 @@ API снова отвечает на запросы.""",
|
|||||||
logger.error('Ошибка проверки API', error=e)
|
logger.error('Ошибка проверки API', error=e)
|
||||||
|
|
||||||
if self._status.api_status:
|
if self._status.api_status:
|
||||||
error_time = format_local_datetime(datetime.utcnow(), '%H:%M:%S %Z')
|
error_time = format_local_datetime(datetime.now(UTC), '%H:%M:%S %Z')
|
||||||
await self._notify_admins(
|
await self._notify_admins(
|
||||||
f"""Ошибка при проверке API Remnawave
|
f"""Ошибка при проверке API Remnawave
|
||||||
|
|
||||||
@@ -442,11 +442,11 @@ API снова отвечает на запросы.""",
|
|||||||
}
|
}
|
||||||
|
|
||||||
async def force_api_check(self) -> dict[str, Any]:
|
async def force_api_check(self) -> dict[str, Any]:
|
||||||
start_time = datetime.utcnow()
|
start_time = datetime.now(UTC)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
api_status = await self.check_api_status()
|
api_status = await self.check_api_status()
|
||||||
end_time = datetime.utcnow()
|
end_time = datetime.now(UTC)
|
||||||
response_time = (end_time - start_time).total_seconds()
|
response_time = (end_time - start_time).total_seconds()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -458,7 +458,7 @@ API снова отвечает на запросы.""",
|
|||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
end_time = datetime.utcnow()
|
end_time = datetime.now(UTC)
|
||||||
response_time = (end_time - start_time).total_seconds()
|
response_time = (end_time - start_time).total_seconds()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -12,14 +12,6 @@ from app.config import settings
|
|||||||
from app.database.models import ButtonClickLog
|
from app.database.models import ButtonClickLog
|
||||||
|
|
||||||
|
|
||||||
def _utcnow() -> datetime:
|
|
||||||
"""Возвращает текущее UTC время как naive datetime (без timezone).
|
|
||||||
|
|
||||||
Замена deprecated datetime.utcnow().
|
|
||||||
"""
|
|
||||||
return datetime.now(UTC).replace(tzinfo=None)
|
|
||||||
|
|
||||||
|
|
||||||
class MenuLayoutStatsService:
|
class MenuLayoutStatsService:
|
||||||
"""Сервис для сбора и анализа статистики кликов по кнопкам."""
|
"""Сервис для сбора и анализа статистики кликов по кнопкам."""
|
||||||
|
|
||||||
@@ -104,7 +96,7 @@ class MenuLayoutStatsService:
|
|||||||
days: int = 30,
|
days: int = 30,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Получить статистику кликов по конкретной кнопке."""
|
"""Получить статистику кликов по конкретной кнопке."""
|
||||||
now = _utcnow()
|
now = datetime.now(UTC)
|
||||||
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
week_ago = now - timedelta(days=7)
|
week_ago = now - timedelta(days=7)
|
||||||
month_ago = now - timedelta(days=days)
|
month_ago = now - timedelta(days=days)
|
||||||
@@ -172,7 +164,7 @@ class MenuLayoutStatsService:
|
|||||||
days: int = 30,
|
days: int = 30,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""Получить статистику кликов по дням."""
|
"""Получить статистику кликов по дням."""
|
||||||
start_date = _utcnow() - timedelta(days=days)
|
start_date = datetime.now(UTC) - timedelta(days=days)
|
||||||
|
|
||||||
# Группировка по дате
|
# Группировка по дате
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
@@ -191,7 +183,7 @@ class MenuLayoutStatsService:
|
|||||||
days: int = 30,
|
days: int = 30,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""Получить статистику по всем кнопкам."""
|
"""Получить статистику по всем кнопкам."""
|
||||||
now = _utcnow()
|
now = datetime.now(UTC)
|
||||||
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
week_ago = now - timedelta(days=7)
|
week_ago = now - timedelta(days=7)
|
||||||
month_ago = now - timedelta(days=days)
|
month_ago = now - timedelta(days=days)
|
||||||
@@ -237,7 +229,7 @@ class MenuLayoutStatsService:
|
|||||||
days: int = 30,
|
days: int = 30,
|
||||||
) -> int:
|
) -> int:
|
||||||
"""Получить общее количество кликов за период."""
|
"""Получить общее количество кликов за период."""
|
||||||
start_date = _utcnow() - timedelta(days=days)
|
start_date = datetime.now(UTC) - timedelta(days=days)
|
||||||
|
|
||||||
result = await db.execute(select(func.count(ButtonClickLog.id)).where(ButtonClickLog.clicked_at >= start_date))
|
result = await db.execute(select(func.count(ButtonClickLog.id)).where(ButtonClickLog.clicked_at >= start_date))
|
||||||
return result.scalar() or 0
|
return result.scalar() or 0
|
||||||
@@ -249,7 +241,7 @@ class MenuLayoutStatsService:
|
|||||||
days: int = 30,
|
days: int = 30,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""Получить статистику кликов по типам кнопок."""
|
"""Получить статистику кликов по типам кнопок."""
|
||||||
start_date = _utcnow() - timedelta(days=days)
|
start_date = datetime.now(UTC) - timedelta(days=days)
|
||||||
|
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(
|
select(
|
||||||
@@ -279,7 +271,7 @@ class MenuLayoutStatsService:
|
|||||||
days: int = 30,
|
days: int = 30,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""Получить статистику кликов по часам дня."""
|
"""Получить статистику кликов по часам дня."""
|
||||||
start_date = _utcnow() - timedelta(days=days)
|
start_date = datetime.now(UTC) - timedelta(days=days)
|
||||||
|
|
||||||
# Используем helper-метод для совместимости с SQLite и PostgreSQL
|
# Используем helper-метод для совместимости с SQLite и PostgreSQL
|
||||||
hour_expr = cls._get_hour_expr(ButtonClickLog.clicked_at).label('hour')
|
hour_expr = cls._get_hour_expr(ButtonClickLog.clicked_at).label('hour')
|
||||||
@@ -311,7 +303,7 @@ class MenuLayoutStatsService:
|
|||||||
Возвращает 0=понедельник, 6=воскресенье.
|
Возвращает 0=понедельник, 6=воскресенье.
|
||||||
Поддерживает как PostgreSQL, так и SQLite.
|
Поддерживает как PostgreSQL, так и SQLite.
|
||||||
"""
|
"""
|
||||||
start_date = _utcnow() - timedelta(days=days)
|
start_date = datetime.now(UTC) - timedelta(days=days)
|
||||||
|
|
||||||
# Используем helper-метод для совместимости с SQLite и PostgreSQL
|
# Используем helper-метод для совместимости с SQLite и PostgreSQL
|
||||||
weekday_expr = cls._get_weekday_expr(ButtonClickLog.clicked_at).label('weekday')
|
weekday_expr = cls._get_weekday_expr(ButtonClickLog.clicked_at).label('weekday')
|
||||||
@@ -345,7 +337,7 @@ class MenuLayoutStatsService:
|
|||||||
days: int = 30,
|
days: int = 30,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""Получить топ пользователей по количеству кликов."""
|
"""Получить топ пользователей по количеству кликов."""
|
||||||
start_date = _utcnow() - timedelta(days=days)
|
start_date = datetime.now(UTC) - timedelta(days=days)
|
||||||
|
|
||||||
query = select(
|
query = select(
|
||||||
ButtonClickLog.user_id,
|
ButtonClickLog.user_id,
|
||||||
@@ -378,7 +370,7 @@ class MenuLayoutStatsService:
|
|||||||
previous_days: int = 7,
|
previous_days: int = 7,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Сравнить статистику текущего и предыдущего периода."""
|
"""Сравнить статистику текущего и предыдущего периода."""
|
||||||
now = _utcnow()
|
now = datetime.now(UTC)
|
||||||
current_start = now - timedelta(days=current_days)
|
current_start = now - timedelta(days=current_days)
|
||||||
previous_start = current_start - timedelta(days=previous_days)
|
previous_start = current_start - timedelta(days=previous_days)
|
||||||
previous_end = current_start
|
previous_end = current_start
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user