fix: use AwareDateTime TypeDecorator for all datetime columns

TypeDecorator with process_result_value guarantees naive datetimes
from pre-TIMESTAMPTZ databases are converted to UTC-aware on every
load. Replaces unreliable event listener approach. All 175 DateTime
columns now use AwareDateTime.
This commit is contained in:
Fringg
2026-02-18 11:11:58 +03:00
parent 38f3a9a16a
commit a7f3d652c5

View File

@@ -25,39 +25,33 @@ from sqlalchemy import (
Table,
Text,
Time,
TypeDecorator,
UniqueConstraint,
event,
inspect as sa_inspect,
)
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import Mapped, backref, mapped_column, relationship
from sqlalchemy.sql import func
Base = declarative_base()
# Cache datetime attribute names per class to avoid repeated mapper inspection
_datetime_attrs_cache: dict[type, list[str]] = {}
@event.listens_for(Base, 'load', propagate=True)
def _ensure_aware_datetimes(target, context):
"""Auto-convert naive datetimes to UTC-aware on load from DB.
class AwareDateTime(TypeDecorator):
"""DateTime that auto-converts naive values to UTC-aware on load from DB.
Handles pre-TIMESTAMPTZ databases that return naive datetimes.
Modifies __dict__ directly to avoid triggering SA dirty tracking.
"""
cls = type(target)
if cls not in _datetime_attrs_cache:
mapper = sa_inspect(cls)
_datetime_attrs_cache[cls] = [
attr.key for attr in mapper.column_attrs if any(isinstance(col.type, DateTime) for col in attr.columns)
]
for key in _datetime_attrs_cache[cls]:
val = target.__dict__.get(key)
if val is not None and isinstance(val, datetime) and val.tzinfo is None:
target.__dict__[key] = val.replace(tzinfo=UTC)
impl = DateTime
cache_ok = True
def __init__(self):
super().__init__(timezone=True)
def process_result_value(self, value, dialect):
if value is not None and isinstance(value, datetime) and value.tzinfo is None:
return value.replace(tzinfo=UTC)
return value
Base = declarative_base()
server_squad_promo_groups = Table(
@@ -210,10 +204,10 @@ class YooKassaPayment(Base):
payment_method_type = Column(String(50), nullable=True)
refundable = Column(Boolean, default=False)
test_mode = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
yookassa_created_at = Column(DateTime(timezone=True), nullable=True)
captured_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
yookassa_created_at = Column(AwareDateTime(), nullable=True)
captured_at = Column(AwareDateTime(), nullable=True)
user = relationship('User', backref='yookassa_payments')
transaction = relationship('Transaction', backref='yookassa_payment')
@@ -259,11 +253,11 @@ class CryptoBotPayment(Base):
mini_app_invoice_url = Column(Text, nullable=True)
web_app_invoice_url = Column(Text, nullable=True)
paid_at = Column(DateTime(timezone=True), nullable=True)
paid_at = Column(AwareDateTime(), nullable=True)
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
user = relationship('User', backref='cryptobot_payments')
transaction = relationship('Transaction', backref='cryptobot_payment')
@@ -311,12 +305,12 @@ class HeleketPayment(Base):
payment_url = Column(Text, nullable=True)
metadata_json = Column(JSON, nullable=True)
paid_at = Column(DateTime(timezone=True), nullable=True)
expires_at = Column(DateTime(timezone=True), nullable=True)
paid_at = Column(AwareDateTime(), nullable=True)
expires_at = Column(AwareDateTime(), nullable=True)
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
user = relationship('User', backref='heleket_payments')
transaction = relationship('Transaction', backref='heleket_payment')
@@ -364,7 +358,7 @@ class MulenPayPayment(Base):
status = Column(String(50), nullable=False, default='created')
is_paid = Column(Boolean, default=False)
paid_at = Column(DateTime(timezone=True), nullable=True)
paid_at = Column(AwareDateTime(), nullable=True)
payment_url = Column(Text, nullable=True)
metadata_json = Column(JSON, nullable=True)
@@ -372,8 +366,8 @@ class MulenPayPayment(Base):
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
user = relationship('User', backref='mulenpay_payments')
transaction = relationship('Transaction', backref='mulenpay_payment')
@@ -402,9 +396,9 @@ class Pal24Payment(Base):
status = Column(String(50), nullable=False, default='NEW')
is_active = Column(Boolean, default=True)
is_paid = Column(Boolean, default=False)
paid_at = Column(DateTime(timezone=True), nullable=True)
paid_at = Column(AwareDateTime(), nullable=True)
last_status = Column(String(50), nullable=True)
last_status_checked_at = Column(DateTime(timezone=True), nullable=True)
last_status_checked_at = Column(AwareDateTime(), nullable=True)
link_url = Column(Text, nullable=True)
link_page_url = Column(Text, nullable=True)
@@ -419,12 +413,12 @@ class Pal24Payment(Base):
payer_account = Column(String(255), nullable=True)
ttl = Column(Integer, nullable=True)
expires_at = Column(DateTime(timezone=True), nullable=True)
expires_at = Column(AwareDateTime(), nullable=True)
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
user = relationship('User', backref='pal24_payments')
transaction = relationship('Transaction', backref='pal24_payment')
@@ -458,7 +452,7 @@ class WataPayment(Base):
status = Column(String(50), nullable=False, default='Opened')
is_paid = Column(Boolean, default=False)
paid_at = Column(DateTime(timezone=True), nullable=True)
paid_at = Column(AwareDateTime(), nullable=True)
last_status = Column(String(50), nullable=True)
terminal_public_id = Column(String(64), nullable=True)
@@ -468,12 +462,12 @@ class WataPayment(Base):
metadata_json = Column(JSON, nullable=True)
callback_payload = Column(JSON, nullable=True)
expires_at = Column(DateTime(timezone=True), nullable=True)
expires_at = Column(AwareDateTime(), nullable=True)
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
user = relationship('User', backref='wata_payments')
transaction = relationship('Transaction', backref='wata_payment')
@@ -501,7 +495,7 @@ class PlategaPayment(Base):
payment_method_code = Column(Integer, nullable=False)
status = Column(String(50), nullable=False, default='PENDING')
is_paid = Column(Boolean, default=False)
paid_at = Column(DateTime(timezone=True), nullable=True)
paid_at = Column(AwareDateTime(), nullable=True)
redirect_url = Column(Text, nullable=True)
return_url = Column(Text, nullable=True)
@@ -510,12 +504,12 @@ class PlategaPayment(Base):
metadata_json = Column(JSON, nullable=True)
callback_payload = Column(JSON, nullable=True)
expires_at = Column(DateTime(timezone=True), nullable=True)
expires_at = Column(AwareDateTime(), nullable=True)
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
user = relationship('User', backref='platega_payments')
transaction = relationship('Transaction', backref='platega_payment')
@@ -544,7 +538,7 @@ class CloudPaymentsPayment(Base):
status = Column(String(50), nullable=False, default='pending') # pending, completed, failed, authorized
is_paid = Column(Boolean, default=False)
paid_at = Column(DateTime(timezone=True), nullable=True)
paid_at = Column(AwareDateTime(), nullable=True)
# Данные карты (маскированные)
card_first_six = Column(String(6), nullable=True)
@@ -571,8 +565,8 @@ class CloudPaymentsPayment(Base):
# Связь с транзакцией в нашей системе
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
user = relationship('User', backref='cloudpayments_payments')
transaction = relationship('Transaction', backref='cloudpayments_payment')
@@ -625,10 +619,10 @@ class FreekassaPayment(Base):
callback_payload = Column(JSON, nullable=True)
# Временные метки
paid_at = Column(DateTime(timezone=True), nullable=True)
expires_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
paid_at = Column(AwareDateTime(), nullable=True)
expires_at = Column(AwareDateTime(), nullable=True)
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
# Связь с транзакцией
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
@@ -687,10 +681,10 @@ class KassaAiPayment(Base):
callback_payload = Column(JSON, nullable=True)
# Временные метки
paid_at = Column(DateTime(timezone=True), nullable=True)
expires_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
paid_at = Column(AwareDateTime(), nullable=True)
expires_at = Column(AwareDateTime(), nullable=True)
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
# Связь с транзакцией
transaction_id = Column(Integer, ForeignKey('transactions.id'), nullable=True)
@@ -732,8 +726,8 @@ class PromoGroup(Base):
auto_assign_total_spent_kopeks = Column(Integer, nullable=True, default=None)
apply_discounts_to_addons = Column(Boolean, nullable=False, default=True)
is_default = Column(Boolean, nullable=False, default=False)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
users = relationship('User', back_populates='promo_group')
user_promo_groups = relationship('UserPromoGroup', back_populates='promo_group', cascade='all, delete-orphan')
@@ -811,7 +805,7 @@ class UserPromoGroup(Base):
user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), primary_key=True)
promo_group_id = Column(Integer, ForeignKey('promo_groups.id', ondelete='CASCADE'), primary_key=True)
assigned_at = Column(DateTime(timezone=True), default=func.now())
assigned_at = Column(AwareDateTime(), default=func.now())
assigned_by = Column(String(50), default='system')
user = relationship('User', back_populates='user_promo_groups')
@@ -885,8 +879,8 @@ class Tariff(Base):
# Режим сброса трафика: DAY, WEEK, MONTH, NO_RESET (по умолчанию берётся из конфига)
traffic_reset_mode = Column(String(20), nullable=True, default=None) # None = использовать глобальную настройку
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
# M2M связь с промогруппами (какие промогруппы имеют доступ к тарифу)
allowed_promo_groups = relationship(
@@ -1019,25 +1013,25 @@ class User(Base):
has_had_paid_subscription = Column(Boolean, default=False, nullable=False)
referred_by_id = Column(Integer, ForeignKey('users.id'), nullable=True, index=True)
referral_code = Column(String(20), unique=True, nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
last_activity = Column(DateTime(timezone=True), default=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
last_activity = Column(AwareDateTime(), default=func.now())
remnawave_uuid = Column(String(255), nullable=True, unique=True)
# Cabinet authentication fields
email = Column(String(255), unique=True, nullable=True, index=True)
email_verified = Column(Boolean, default=False, nullable=False)
email_verified_at = Column(DateTime(timezone=True), nullable=True)
email_verified_at = Column(AwareDateTime(), nullable=True)
password_hash = Column(String(255), nullable=True)
email_verification_token = Column(String(255), nullable=True)
email_verification_expires = Column(DateTime(timezone=True), nullable=True)
email_verification_expires = Column(AwareDateTime(), nullable=True)
password_reset_token = Column(String(255), nullable=True)
password_reset_expires = Column(DateTime(timezone=True), nullable=True)
cabinet_last_login = Column(DateTime(timezone=True), nullable=True)
password_reset_expires = Column(AwareDateTime(), nullable=True)
cabinet_last_login = Column(AwareDateTime(), nullable=True)
# Email change fields
email_change_new = Column(String(255), nullable=True) # New email pending verification
email_change_code = Column(String(6), nullable=True) # 6-digit verification code
email_change_expires = Column(DateTime(timezone=True), nullable=True) # Code expiration
email_change_expires = Column(AwareDateTime(), nullable=True) # Code expiration
# OAuth provider IDs
google_id = Column(String(255), unique=True, nullable=True, index=True)
yandex_id = Column(String(255), unique=True, nullable=True, index=True)
@@ -1056,8 +1050,8 @@ class User(Base):
referral_commission_percent = Column(Integer, nullable=True)
promo_offer_discount_percent = Column(Integer, nullable=False, default=0)
promo_offer_discount_source = Column(String(100), nullable=True)
promo_offer_discount_expires_at = Column(DateTime(timezone=True), nullable=True)
last_remnawave_sync = Column(DateTime(timezone=True), nullable=True)
promo_offer_discount_expires_at = Column(AwareDateTime(), nullable=True)
last_remnawave_sync = Column(AwareDateTime(), nullable=True)
trojan_password = Column(String(255), nullable=True)
vless_uuid = Column(String(255), nullable=True)
ss_password = Column(String(255), nullable=True)
@@ -1164,14 +1158,14 @@ class Subscription(Base):
status = Column(String(20), default=SubscriptionStatus.TRIAL.value)
is_trial = Column(Boolean, default=True)
start_date = Column(DateTime(timezone=True), default=func.now())
end_date = Column(DateTime(timezone=True), nullable=False)
start_date = Column(AwareDateTime(), default=func.now())
end_date = Column(AwareDateTime(), nullable=False)
traffic_limit_gb = Column(Integer, default=0)
traffic_used_gb = Column(Float, default=0.0)
purchased_traffic_gb = Column(Integer, default=0) # Докупленный трафик
traffic_reset_at = Column(
DateTime(timezone=True), nullable=True
AwareDateTime(), nullable=True
) # Дата сброса докупленного трафика (30 дней после первой докупки)
subscription_url = Column(String, nullable=True)
@@ -1185,10 +1179,10 @@ class Subscription(Base):
autopay_enabled = Column(Boolean, default=False)
autopay_days_before = Column(Integer, default=3)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
last_webhook_update_at = Column(DateTime(timezone=True), nullable=True)
last_webhook_update_at = Column(AwareDateTime(), nullable=True)
remnawave_short_uuid = Column(String(255), nullable=True)
@@ -1199,7 +1193,7 @@ class Subscription(Base):
is_daily_paused = Column(
Boolean, default=False, nullable=False
) # Приостановлена ли суточная подписка пользователем
last_daily_charge_at = Column(DateTime(timezone=True), nullable=True) # Время последнего суточного списания
last_daily_charge_at = Column(AwareDateTime(), nullable=True) # Время последнего суточного списания
user = relationship('User', back_populates='subscription')
tariff = relationship('Tariff', back_populates='subscriptions')
@@ -1372,9 +1366,9 @@ class TrafficPurchase(Base):
subscription_id = Column(Integer, ForeignKey('subscriptions.id', ondelete='CASCADE'), nullable=False, index=True)
traffic_gb = Column(Integer, nullable=False) # Количество ГБ в покупке
expires_at = Column(DateTime(timezone=True), nullable=False, index=True) # Дата истечения (покупка + 30 дней)
expires_at = Column(AwareDateTime(), nullable=False, index=True) # Дата истечения (покупка + 30 дней)
created_at = Column(DateTime(timezone=True), default=func.now())
created_at = Column(AwareDateTime(), default=func.now())
subscription = relationship('Subscription', back_populates='traffic_purchases')
@@ -1401,10 +1395,10 @@ class Transaction(Base):
# NaloGO чек
receipt_uuid = Column(String(255), nullable=True, index=True)
receipt_created_at = Column(DateTime(timezone=True), nullable=True)
receipt_created_at = Column(AwareDateTime(), nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
completed_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(AwareDateTime(), default=func.now())
completed_at = Column(AwareDateTime(), nullable=True)
user = relationship('User', back_populates='transactions')
@@ -1419,7 +1413,7 @@ class SubscriptionConversion(Base):
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
converted_at = Column(DateTime(timezone=True), default=func.now())
converted_at = Column(AwareDateTime(), default=func.now())
trial_duration_days = Column(Integer, nullable=True)
@@ -1429,7 +1423,7 @@ class SubscriptionConversion(Base):
first_paid_period_days = Column(Integer, nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
created_at = Column(AwareDateTime(), default=func.now())
user = relationship('User', backref='subscription_conversions')
@@ -1455,8 +1449,8 @@ class PromoCode(Base):
max_uses = Column(Integer, default=1)
current_uses = Column(Integer, default=0)
valid_from = Column(DateTime(timezone=True), default=func.now())
valid_until = Column(DateTime(timezone=True), nullable=True)
valid_from = Column(AwareDateTime(), default=func.now())
valid_until = Column(AwareDateTime(), nullable=True)
is_active = Column(Boolean, default=True)
first_purchase_only = Column(Boolean, default=False) # Только для первой покупки
@@ -1464,8 +1458,8 @@ class PromoCode(Base):
created_by = Column(Integer, ForeignKey('users.id'), nullable=True)
promo_group_id = Column(Integer, ForeignKey('promo_groups.id', ondelete='SET NULL'), nullable=True, index=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
uses = relationship('PromoCodeUse', back_populates='promocode')
promo_group = relationship('PromoGroup')
@@ -1492,7 +1486,7 @@ class PromoCodeUse(Base):
promocode_id = Column(Integer, ForeignKey('promocodes.id'), nullable=False)
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
used_at = Column(DateTime(timezone=True), default=func.now())
used_at = Column(AwareDateTime(), default=func.now())
promocode = relationship('PromoCode', back_populates='uses')
user = relationship('User')
@@ -1513,7 +1507,7 @@ class ReferralEarning(Base):
Integer, ForeignKey('advertising_campaigns.id', ondelete='SET NULL'), nullable=True, index=True
)
created_at = Column(DateTime(timezone=True), default=func.now())
created_at = Column(AwareDateTime(), default=func.now())
user = relationship('User', foreign_keys=[user_id], back_populates='referral_earnings')
referral = relationship('User', foreign_keys=[referral_id])
@@ -1555,11 +1549,11 @@ class WithdrawalRequest(Base):
# Обработка админом
processed_by = Column(Integer, ForeignKey('users.id'), nullable=True)
processed_at = Column(DateTime(timezone=True), nullable=True)
processed_at = Column(AwareDateTime(), nullable=True)
admin_comment = Column(Text, nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
user = relationship('User', foreign_keys=[user_id], backref='withdrawal_requests')
admin = relationship('User', foreign_keys=[processed_by])
@@ -1589,10 +1583,10 @@ class PartnerApplication(Base):
admin_comment = Column(Text, nullable=True)
approved_commission_percent = Column(Integer, nullable=True)
processed_by = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True)
processed_at = Column(DateTime(timezone=True), nullable=True)
processed_at = Column(AwareDateTime(), nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
user = relationship('User', foreign_keys=[user_id], backref='partner_applications')
admin = relationship('User', foreign_keys=[processed_by])
@@ -1606,18 +1600,18 @@ class ReferralContest(Base):
description = Column(Text, nullable=True)
prize_text = Column(Text, nullable=True)
contest_type = Column(String(50), nullable=False, default='referral_paid')
start_at = Column(DateTime(timezone=True), nullable=False)
end_at = Column(DateTime(timezone=True), nullable=False)
start_at = Column(AwareDateTime(), nullable=False)
end_at = Column(AwareDateTime(), nullable=False)
daily_summary_time = Column(Time, nullable=False, default=time(hour=12, minute=0))
daily_summary_times = Column(String(255), nullable=True) # CSV HH:MM
timezone = Column(String(64), nullable=False, default='UTC')
is_active = Column(Boolean, nullable=False, default=True)
last_daily_summary_date = Column(Date, nullable=True)
last_daily_summary_at = Column(DateTime(timezone=True), nullable=True)
last_daily_summary_at = Column(AwareDateTime(), nullable=True)
final_summary_sent = Column(Boolean, nullable=False, default=False)
created_by = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
creator = relationship('User', backref='created_referral_contests')
events = relationship(
@@ -1647,7 +1641,7 @@ class ReferralContestEvent(Base):
referral_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), nullable=False)
event_type = Column(String(50), nullable=False)
amount_kopeks = Column(Integer, nullable=False, default=0)
occurred_at = Column(DateTime(timezone=True), nullable=False, default=func.now())
occurred_at = Column(AwareDateTime(), nullable=False, default=func.now())
contest = relationship('ReferralContest', back_populates='events')
referrer = relationship('User', foreign_keys=[referrer_id])
@@ -1667,7 +1661,7 @@ class ReferralContestVirtualParticipant(Base):
display_name = Column(String(255), nullable=False)
referral_count = Column(Integer, nullable=False, default=0)
total_amount_kopeks = Column(Integer, nullable=False, default=0)
created_at = Column(DateTime(timezone=True), default=func.now())
created_at = Column(AwareDateTime(), default=func.now())
contest = relationship('ReferralContest')
@@ -1693,8 +1687,8 @@ class ContestTemplate(Base):
cooldown_hours = Column(Integer, nullable=False, default=24)
payload = Column(JSON, nullable=True)
is_enabled = Column(Boolean, nullable=False, default=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
rounds = relationship('ContestRound', back_populates='template')
@@ -1708,8 +1702,8 @@ class ContestRound(Base):
id = Column(Integer, primary_key=True, index=True)
template_id = Column(Integer, ForeignKey('contest_templates.id', ondelete='CASCADE'), nullable=False)
starts_at = Column(DateTime(timezone=True), nullable=False)
ends_at = Column(DateTime(timezone=True), nullable=False)
starts_at = Column(AwareDateTime(), nullable=False)
ends_at = Column(AwareDateTime(), nullable=False)
status = Column(String(20), nullable=False, default='active') # active, finished
payload = Column(JSON, nullable=True)
winners_count = Column(Integer, nullable=False, default=0)
@@ -1717,8 +1711,8 @@ class ContestRound(Base):
attempts_per_user = Column(Integer, nullable=False, default=1)
message_id = Column(BigInteger, nullable=True)
chat_id = Column(BigInteger, nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
template = relationship('ContestTemplate', back_populates='rounds')
attempts = relationship('ContestAttempt', back_populates='round', cascade='all, delete-orphan')
@@ -1736,7 +1730,7 @@ class ContestAttempt(Base):
user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), nullable=False)
answer = Column(Text, nullable=True)
is_winner = Column(Boolean, nullable=False, default=False)
created_at = Column(DateTime(timezone=True), default=func.now())
created_at = Column(AwareDateTime(), default=func.now())
round = relationship('ContestRound', back_populates='attempts')
user = relationship('User')
@@ -1756,8 +1750,8 @@ class Squad(Base):
description = Column(Text, nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
@property
def price_rubles(self) -> float:
@@ -1778,8 +1772,8 @@ class ServiceRule(Base):
language = Column(String(5), default='ru')
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
class PrivacyPolicy(Base):
@@ -1789,8 +1783,8 @@ class PrivacyPolicy(Base):
language = Column(String(10), nullable=False, unique=True)
content = Column(Text, nullable=False)
is_enabled = Column(Boolean, default=True, nullable=False)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
class PublicOffer(Base):
@@ -1800,8 +1794,8 @@ class PublicOffer(Base):
language = Column(String(10), nullable=False, unique=True)
content = Column(Text, nullable=False)
is_enabled = Column(Boolean, default=True, nullable=False)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
class FaqSetting(Base):
@@ -1810,8 +1804,8 @@ class FaqSetting(Base):
id = Column(Integer, primary_key=True, index=True)
language = Column(String(10), nullable=False, unique=True)
is_enabled = Column(Boolean, default=True, nullable=False)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
class FaqPage(Base):
@@ -1823,8 +1817,8 @@ class FaqPage(Base):
content = Column(Text, nullable=False)
display_order = Column(Integer, default=0, nullable=False)
is_active = Column(Boolean, default=True, nullable=False)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
class SystemSetting(Base):
@@ -1835,8 +1829,8 @@ class SystemSetting(Base):
value = Column(Text, nullable=True)
description = Column(Text, nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
class MonitoringLog(Base):
@@ -1851,7 +1845,7 @@ class MonitoringLog(Base):
is_success = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), default=func.now())
created_at = Column(AwareDateTime(), default=func.now())
class SentNotification(Base):
@@ -1862,7 +1856,7 @@ class SentNotification(Base):
subscription_id = Column(Integer, ForeignKey('subscriptions.id', ondelete='CASCADE'), nullable=False)
notification_type = Column(String(50), nullable=False)
days_before = Column(Integer, nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
created_at = Column(AwareDateTime(), default=func.now())
user = relationship('User', backref='sent_notifications')
subscription = relationship('Subscription', backref=backref('sent_notifications', passive_deletes=True))
@@ -1879,9 +1873,9 @@ class SubscriptionEvent(Base):
amount_kopeks = Column(Integer, nullable=True)
currency = Column(String(16), nullable=True)
message = Column(Text, nullable=True)
occurred_at = Column(DateTime(timezone=True), nullable=False, default=func.now())
occurred_at = Column(AwareDateTime(), nullable=False, default=func.now())
extra = Column(JSON, nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
created_at = Column(AwareDateTime(), default=func.now())
user = relationship('User', backref='subscription_events')
subscription = relationship('Subscription', backref='subscription_events')
@@ -1898,13 +1892,13 @@ class DiscountOffer(Base):
notification_type = Column(String(50), nullable=False)
discount_percent = Column(Integer, nullable=False, default=0)
bonus_amount_kopeks = Column(Integer, nullable=False, default=0)
expires_at = Column(DateTime(timezone=True), nullable=False)
claimed_at = Column(DateTime(timezone=True), nullable=True)
expires_at = Column(AwareDateTime(), nullable=False)
claimed_at = Column(AwareDateTime(), nullable=True)
is_active = Column(Boolean, default=True, nullable=False)
effect_type = Column(String(50), nullable=False, default='percent_discount')
extra_data = Column(JSON, nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
user = relationship('User', back_populates='discount_offers')
subscription = relationship('Subscription', back_populates='discount_offers')
@@ -1928,8 +1922,8 @@ class PromoOfferTemplate(Base):
test_squad_uuids = Column(JSON, default=list)
is_active = Column(Boolean, default=True, nullable=False)
created_by = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
creator = relationship('User')
@@ -1941,9 +1935,9 @@ class SubscriptionTemporaryAccess(Base):
subscription_id = Column(Integer, ForeignKey('subscriptions.id', ondelete='CASCADE'), nullable=False)
offer_id = Column(Integer, ForeignKey('discount_offers.id', ondelete='CASCADE'), nullable=False)
squad_uuid = Column(String(255), nullable=False)
expires_at = Column(DateTime(timezone=True), nullable=False)
created_at = Column(DateTime(timezone=True), default=func.now())
deactivated_at = Column(DateTime(timezone=True), nullable=True)
expires_at = Column(AwareDateTime(), nullable=False)
created_at = Column(AwareDateTime(), default=func.now())
deactivated_at = Column(AwareDateTime(), nullable=True)
is_active = Column(Boolean, default=True, nullable=False)
was_already_connected = Column(Boolean, default=False, nullable=False)
@@ -1962,7 +1956,7 @@ class PromoOfferLog(Base):
percent = Column(Integer, nullable=True)
effect_type = Column(String(50), nullable=True)
details = Column(JSON, nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
created_at = Column(AwareDateTime(), default=func.now())
user = relationship('User', back_populates='promo_offer_logs')
offer = relationship('DiscountOffer', back_populates='logs')
@@ -1985,8 +1979,8 @@ class BroadcastHistory(Base):
status = Column(String(50), default='in_progress')
admin_id = Column(Integer, ForeignKey('users.id'))
admin_name = Column(String(255))
created_at = Column(DateTime(timezone=True), server_default=func.now())
completed_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(AwareDateTime(), server_default=func.now())
completed_at = Column(AwareDateTime(), nullable=True)
# Email broadcast fields
channel = Column(String(20), default='telegram', nullable=False) # telegram|email|both
@@ -2005,8 +1999,8 @@ class Poll(Base):
reward_enabled = Column(Boolean, nullable=False, default=False)
reward_amount_kopeks = Column(Integer, nullable=False, default=0)
created_by = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now(), nullable=False)
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now(), nullable=False)
created_at = Column(AwareDateTime(), default=func.now(), nullable=False)
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now(), nullable=False)
creator = relationship('User', backref='created_polls', foreign_keys=[created_by])
questions = relationship(
@@ -2058,9 +2052,9 @@ class PollResponse(Base):
id = Column(Integer, primary_key=True, index=True)
poll_id = Column(Integer, ForeignKey('polls.id', ondelete='CASCADE'), nullable=False, index=True)
user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), nullable=False, index=True)
sent_at = Column(DateTime(timezone=True), default=func.now(), nullable=False)
started_at = Column(DateTime(timezone=True), nullable=True)
completed_at = Column(DateTime(timezone=True), nullable=True)
sent_at = Column(AwareDateTime(), default=func.now(), nullable=False)
started_at = Column(AwareDateTime(), nullable=True)
completed_at = Column(AwareDateTime(), nullable=True)
reward_given = Column(Boolean, nullable=False, default=False)
reward_amount_kopeks = Column(Integer, nullable=False, default=0)
@@ -2082,7 +2076,7 @@ class PollAnswer(Base):
response_id = Column(Integer, ForeignKey('poll_responses.id', ondelete='CASCADE'), nullable=False, index=True)
question_id = Column(Integer, ForeignKey('poll_questions.id', ondelete='CASCADE'), nullable=False, index=True)
option_id = Column(Integer, ForeignKey('poll_options.id', ondelete='CASCADE'), nullable=False, index=True)
created_at = Column(DateTime(timezone=True), default=func.now(), nullable=False)
created_at = Column(AwareDateTime(), default=func.now(), nullable=False)
response = relationship('PollResponse', back_populates='answers')
question = relationship('PollQuestion', back_populates='answers')
@@ -2116,8 +2110,8 @@ class ServerSquad(Base):
max_users = Column(Integer, nullable=True)
current_users = Column(Integer, default=0)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
allowed_promo_groups = relationship(
'PromoGroup',
@@ -2152,7 +2146,7 @@ class SubscriptionServer(Base):
subscription_id = Column(Integer, ForeignKey('subscriptions.id'), nullable=False)
server_squad_id = Column(Integer, ForeignKey('server_squads.id'), nullable=False)
connected_at = Column(DateTime(timezone=True), default=func.now())
connected_at = Column(AwareDateTime(), default=func.now())
paid_price_kopeks = Column(Integer, default=0)
@@ -2171,7 +2165,7 @@ class SupportAuditLog(Base):
ticket_id = Column(Integer, ForeignKey('tickets.id', ondelete='SET NULL'), nullable=True)
target_user_id = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True)
details = Column(JSON, nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
created_at = Column(AwareDateTime(), default=func.now())
actor = relationship('User', foreign_keys=[actor_user_id])
ticket = relationship('Ticket', foreign_keys=[ticket_id])
@@ -2184,8 +2178,8 @@ class UserMessage(Base):
is_active = Column(Boolean, default=True)
sort_order = Column(Integer, default=0)
created_by = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
creator = relationship('User', backref='created_messages')
def __repr__(self):
@@ -2200,8 +2194,8 @@ class WelcomeText(Base):
is_active = Column(Boolean, default=True)
is_enabled = Column(Boolean, default=True)
created_by = Column(Integer, ForeignKey('users.id'), nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
creator = relationship('User', backref='created_welcome_texts')
@@ -2217,8 +2211,8 @@ class PinnedMessage(Base):
send_on_every_start = Column(Boolean, nullable=False, server_default='1', default=True)
is_active = Column(Boolean, default=True)
created_by = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
creator = relationship('User', backref='pinned_messages')
@@ -2248,8 +2242,8 @@ class AdvertisingCampaign(Base):
partner_user_id = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True, index=True)
created_by = Column(Integer, ForeignKey('users.id'), nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
registrations = relationship('AdvertisingCampaignRegistration', back_populates='campaign')
tariff = relationship('Tariff', foreign_keys=[tariff_id])
@@ -2290,7 +2284,7 @@ class AdvertisingCampaignRegistration(Base):
tariff_id = Column(Integer, ForeignKey('tariffs.id', ondelete='SET NULL'), nullable=True)
tariff_duration_days = Column(Integer, nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
created_at = Column(AwareDateTime(), default=func.now())
campaign = relationship('AdvertisingCampaign', back_populates='registrations')
user = relationship('User')
@@ -2319,13 +2313,13 @@ class Ticket(Base):
priority = Column(String(20), default='normal', nullable=False) # low, normal, high, urgent
# Блокировка ответов пользователя в этом тикете
user_reply_block_permanent = Column(Boolean, default=False, nullable=False)
user_reply_block_until = Column(DateTime(timezone=True), nullable=True)
user_reply_block_until = Column(AwareDateTime(), nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
closed_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
closed_at = Column(AwareDateTime(), nullable=True)
# SLA reminders
last_sla_reminder_at = Column(DateTime(timezone=True), nullable=True)
last_sla_reminder_at = Column(AwareDateTime(), nullable=True)
# Связи
user = relationship('User', backref='tickets')
@@ -2390,7 +2384,7 @@ class TicketMessage(Base):
media_file_id = Column(String(255), nullable=True)
media_caption = Column(Text, nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
created_at = Column(AwareDateTime(), default=func.now())
# Связи
ticket = relationship('Ticket', back_populates='messages')
@@ -2416,10 +2410,10 @@ class WebApiToken(Base):
token_hash = Column(String(128), nullable=False, unique=True, index=True)
token_prefix = Column(String(32), nullable=False, index=True)
description = Column(Text, nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
expires_at = Column(DateTime(timezone=True), nullable=True)
last_used_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
expires_at = Column(AwareDateTime(), nullable=True)
last_used_at = Column(AwareDateTime(), nullable=True)
last_used_ip = Column(String(64), nullable=True)
is_active = Column(Boolean, default=True, nullable=False)
created_by = Column(String(255), nullable=True)
@@ -2439,8 +2433,8 @@ class MainMenuButton(Base):
visibility = Column(String(20), nullable=False, default=MainMenuButtonVisibility.ALL.value)
is_active = Column(Boolean, nullable=False, default=True)
display_order = Column(Integer, nullable=False, default=0)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
__table_args__ = (Index('ix_main_menu_buttons_order', 'display_order', 'id'),)
@@ -2475,7 +2469,7 @@ class MenuLayoutHistory(Base):
action = Column(String(50), nullable=False) # update, reset, import
changes_summary = Column(Text, nullable=True) # Краткое описание изменений
user_info = Column(String(255), nullable=True) # Информация о пользователе/токене
created_at = Column(DateTime(timezone=True), default=func.now(), index=True)
created_at = Column(AwareDateTime(), default=func.now(), index=True)
__table_args__ = (Index('ix_menu_layout_history_created', 'created_at'),)
@@ -2492,7 +2486,7 @@ class ButtonClickLog(Base):
button_id = Column(String(100), nullable=False, index=True) # ID кнопки
user_id = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True, index=True)
callback_data = Column(String(255), nullable=True) # callback_data кнопки
clicked_at = Column(DateTime(timezone=True), default=func.now(), index=True)
clicked_at = Column(AwareDateTime(), default=func.now(), index=True)
# Дополнительная информация
button_type = Column(String(20), nullable=True, index=True) # builtin, callback, url, mini_app
@@ -2526,9 +2520,9 @@ class Webhook(Base):
event_type = Column(String(50), nullable=False) # user.created, payment.completed, ticket.created, etc.
is_active = Column(Boolean, default=True, nullable=False)
description = Column(Text, nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
last_triggered_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
last_triggered_at = Column(AwareDateTime(), nullable=True)
failure_count = Column(Integer, default=0, nullable=False)
success_count = Column(Integer, default=0, nullable=False)
@@ -2557,9 +2551,9 @@ class WebhookDelivery(Base):
status = Column(String(20), nullable=False) # pending, success, failed
error_message = Column(Text, nullable=True)
attempt_number = Column(Integer, default=1, nullable=False)
created_at = Column(DateTime(timezone=True), default=func.now())
delivered_at = Column(DateTime(timezone=True), nullable=True)
next_retry_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(AwareDateTime(), default=func.now())
delivered_at = Column(AwareDateTime(), nullable=True)
next_retry_at = Column(AwareDateTime(), nullable=True)
webhook = relationship('Webhook', back_populates='deliveries')
@@ -2577,9 +2571,9 @@ class CabinetRefreshToken(Base):
user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), nullable=False)
token_hash = Column(String(255), unique=True, nullable=False, index=True)
device_info = Column(String(500), nullable=True)
expires_at = Column(DateTime(timezone=True), nullable=False)
created_at = Column(DateTime(timezone=True), default=func.now())
revoked_at = Column(DateTime(timezone=True), nullable=True)
expires_at = Column(AwareDateTime(), nullable=False)
created_at = Column(AwareDateTime(), default=func.now())
revoked_at = Column(AwareDateTime(), nullable=True)
user = relationship('User', backref='cabinet_tokens')
@@ -2631,8 +2625,8 @@ class WheelConfig(Base):
promo_prefix = Column(String(20), default='WHEEL', nullable=False)
promo_validity_days = Column(Integer, default=7, nullable=False)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
prizes = relationship('WheelPrize', back_populates='config', cascade='all, delete-orphan')
@@ -2670,8 +2664,8 @@ class WheelPrize(Base):
promo_subscription_days = Column(Integer, default=0)
promo_traffic_gb = Column(Integer, default=0)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
config = relationship('WheelConfig', back_populates='prizes')
spins = relationship('WheelSpin', back_populates='prize')
@@ -2706,9 +2700,9 @@ class WheelSpin(Base):
# Флаг успешного начисления
is_applied = Column(Boolean, default=False, nullable=False)
applied_at = Column(DateTime(timezone=True), nullable=True)
applied_at = Column(AwareDateTime(), nullable=True)
created_at = Column(DateTime(timezone=True), default=func.now())
created_at = Column(AwareDateTime(), default=func.now())
user = relationship('User', backref='wheel_spins')
prize = relationship('WheelPrize', back_populates='spins')
@@ -2753,8 +2747,8 @@ class TicketNotification(Base):
# Прочитано ли уведомление
is_read = Column(Boolean, default=False, nullable=False)
created_at = Column(DateTime(timezone=True), default=func.now())
read_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column(AwareDateTime(), default=func.now())
read_at = Column(AwareDateTime(), nullable=True)
ticket = relationship('Ticket', backref='notifications')
user = relationship('User', backref='ticket_notifications')
@@ -2811,8 +2805,8 @@ class PaymentMethodConfig(Base):
lazy='selectin',
)
created_at = Column(DateTime(timezone=True), default=func.now())
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
created_at = Column(AwareDateTime(), default=func.now())
updated_at = Column(AwareDateTime(), default=func.now(), onupdate=func.now())
def __repr__(self) -> str:
return f"<PaymentMethodConfig method_id='{self.method_id}' order={self.sort_order} enabled={self.is_enabled}>"