Compare commits

...

33 Commits

Author SHA1 Message Date
Egor 330d670f3f Merge pull request #2621 from BEDOLAGA-DEV/release-please--branches--main
chore(main): release 3.15.1
2026-02-17 07:55:52 +03:00
github-actions[bot] 41cb122a2f chore(main): release 3.15.1 2026-02-17 04:54:40 +00:00
Egor 1b3e6f2f11 Merge pull request #2620 from BEDOLAGA-DEV/dev
fix: add naive datetime guards to fromisoformat() in Redis cache readers
2026-02-17 07:54:20 +03:00
Fringg 6fa49485d9 fix: add naive datetime guards to fromisoformat() in Redis cache readers
Old Redis entries saved before utcnow→now(UTC) migration lack timezone
info, causing TypeError on subtraction with aware datetimes.
2026-02-17 07:52:26 +03:00
Egor 71aa023133 Merge pull request #2619 from BEDOLAGA-DEV/release-please--branches--main
chore(main): release 3.15.0
2026-02-17 07:25:32 +03:00
github-actions[bot] e567c02658 chore(main): release 3.15.0 2026-02-17 04:01:31 +00:00
Egor f393dc0840 Merge pull request #2618 from BEDOLAGA-DEV/dev
Dev
2026-02-17 07:01:07 +03:00
Fringg 5dc4b0ec15 chore: ruff format oauth.py, auth schemas, admin_notification_service 2026-02-17 06:57:30 +03:00
Fringg e68760cc66 fix: remove local UTC re-imports shadowing module-level import in purchase.py
Caused UnboundLocalError on datetime.now(UTC) at line 209 because
Python treats the function-local `from datetime import UTC` (lines 351, 362)
as a local variable declaration, making UTC unbound before those lines.
2026-02-17 06:45:46 +03:00
Fringg d9552799c1 feat: add web campaign links with bonus processing in auth flow
- Add web_link generation for campaigns (uses MINIAPP_CUSTOM_URL)
- Process campaign_slug in all auth endpoints (telegram, widget, email, oauth)
- Apply campaign bonus (balance/subscription/tariff) with SELECT FOR UPDATE lock
- Add rollback + user refresh on campaign bonus failure
- Fix N+1 query in campaign registrations (batch subscription check)
- Remove duplicate queries in get_campaign_statistics (~60 lines dead code)
- Simplify _store_refresh_token (remove TOCTOU pre-check, keep IntegrityError)
- Remove dead expression in campaign_service.py
- Align start_parameter max_length to 64 (matches DB column)
- Remove unused campaign_slug from EmailRegisterStandaloneRequest
2026-02-17 06:44:03 +03:00
Fringg c75ec0b22a fix: AttributeError in withdrawal admin notification (send_to_admins → send_admin_notification) 2026-02-17 05:23:49 +03:00
Fringg 27309f53d9 feat: add LOG_COLORS env setting to toggle console ANSI colors 2026-02-17 05:15:03 +03:00
Egor 4193f717ee Merge pull request #2617 from BEDOLAGA-DEV/release-please--branches--main
chore(main): release 3.14.1
2026-02-17 05:04:03 +03:00
github-actions[bot] d297985b0b chore(main): release 3.14.1 2026-02-17 02:03:34 +00:00
Egor 6dcf3a9f0d Merge pull request #2616 from BEDOLAGA-DEV/dev
Dev
2026-02-17 05:03:06 +03:00
Fringg 094609005a fix: add naive datetime guards to parsers and fix test datetime literals 2026-02-17 05:00:13 +03:00
Fringg eb18994b7d fix: complete datetime.utcnow() → datetime.now(UTC) migration
- Migrate 660+ datetime.utcnow() across 153 files to datetime.now(UTC)
- Migrate 30+ datetime.now() without UTC to datetime.now(UTC)
- Convert all 170 DateTime columns to DateTime(timezone=True)
- Add migrate_datetime_to_timestamptz() in universal_migration with SET LOCAL timezone='UTC' safety
- Remove 70+ .replace(tzinfo=None) workarounds
- Fix utcfromtimestamp → fromtimestamp(..., tz=UTC)
- Fix fromtimestamp() without tz= (system_logs, backup_service, referral_diagnostics)
- Fix fromisoformat/isoparse to ensure aware output (platega, yookassa, wata, miniapp, nalogo)
- Fix strptime() to add .replace(tzinfo=UTC) (backup_service, referral_diagnostics)
- Fix datetime.combine() to include tzinfo=UTC (remnawave_sync, traffic_monitoring)
- Fix datetime.max/datetime.min sentinels with .replace(tzinfo=UTC)
- Rename panel_datetime_to_naive_utc → panel_datetime_to_utc
- Remove DTZ003 from ruff ignore list
2026-02-17 04:45:40 +03:00
Fringg ff21b27b98 fix: address remaining abs() issues from review
- admin_traffic._get_bulk_spending: add func.abs() for SUBSCRIPTION_PAYMENT SUM
- get_user_total_spent_kopeks: move abs() from Python to SQL (per-row func.abs)
- referral_contest.total_outside: add abs() for mixed-type sum
- Revert func.abs() from generic by_type aggregation to preserve refund/withdrawal signs
2026-02-17 03:47:39 +03:00
Fringg 4247981c98 fix: normalize transaction amount signs across all aggregations
SUBSCRIPTION_PAYMENT transactions have inconsistent signs in DB
(some negative, some positive). Add func.abs()/abs() to all SUM
queries and display code to ensure correct totals regardless of sign.

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

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