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