Files
remnawave-bedolaga-telegram…/database.py
2025-08-13 07:49:40 +03:00

2574 lines
113 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import BigInteger, String, Float, DateTime, Boolean, Text, Integer, text, select, func, and_, Column
from datetime import datetime, timedelta
from typing import Optional, List, Dict, Any
import logging
logger = logging.getLogger(__name__)
class Base(DeclarativeBase):
pass
class ReferralProgram(Base):
__tablename__ = 'referral_programs'
id: Mapped[int] = mapped_column(primary_key=True)
referrer_id: Mapped[int] = mapped_column(BigInteger, index=True)
referred_id: Mapped[int] = mapped_column(BigInteger, unique=True, index=True)
referral_code: Mapped[str] = mapped_column(String(20), index=True)
first_reward_paid: Mapped[bool] = mapped_column(Boolean, default=False)
total_earned: Mapped[float] = mapped_column(Float, default=0.0)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
first_reward_at: Mapped[Optional[datetime]] = mapped_column(DateTime)
class ReferralEarning(Base):
__tablename__ = 'referral_earnings'
id: Mapped[int] = mapped_column(primary_key=True)
referrer_id: Mapped[int] = mapped_column(BigInteger, index=True)
referred_id: Mapped[int] = mapped_column(BigInteger, index=True)
amount: Mapped[float] = mapped_column(Float)
earning_type: Mapped[str] = mapped_column(String(20))
related_payment_id: Mapped[Optional[int]] = mapped_column(Integer)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
class User(Base):
__tablename__ = 'users'
id: Mapped[int] = mapped_column(primary_key=True)
telegram_id: Mapped[int] = mapped_column(BigInteger, unique=True, index=True)
username: Mapped[Optional[str]] = mapped_column(String(255))
first_name: Mapped[Optional[str]] = mapped_column(String(255))
last_name: Mapped[Optional[str]] = mapped_column(String(255))
language: Mapped[str] = mapped_column(String(10), default='ru')
balance: Mapped[float] = mapped_column(Float, default=0.0)
is_admin: Mapped[bool] = mapped_column(Boolean, default=False)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
remnawave_uuid: Mapped[Optional[str]] = mapped_column(String(255))
is_trial_used: Mapped[bool] = mapped_column(Boolean, default=False)
class Subscription(Base):
__tablename__ = 'subscriptions'
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(255))
description: Mapped[Optional[str]] = mapped_column(Text)
price: Mapped[float] = mapped_column(Float)
duration_days: Mapped[int] = mapped_column(Integer)
traffic_limit_gb: Mapped[int] = mapped_column(Integer, default=0)
squad_uuid: Mapped[str] = mapped_column(String(255))
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
is_trial: Mapped[bool] = mapped_column(Boolean, default=False)
is_imported: Mapped[bool] = mapped_column(Boolean, default=False)
class UserSubscription(Base):
__tablename__ = 'user_subscriptions'
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(BigInteger, index=True)
subscription_id: Mapped[int] = mapped_column(Integer, index=True)
short_uuid: Mapped[str] = mapped_column(String(255))
expires_at: Mapped[datetime] = mapped_column(DateTime)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
traffic_limit_gb: Mapped[Optional[int]] = mapped_column(Integer)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime, onupdate=datetime.utcnow)
auto_pay_enabled: Mapped[bool] = mapped_column(Boolean, default=False)
auto_pay_days_before: Mapped[int] = mapped_column(Integer, default=3)
class Payment(Base):
__tablename__ = 'payments'
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(BigInteger, index=True)
amount: Mapped[float] = mapped_column(Float)
payment_type: Mapped[str] = mapped_column(String(50))
description: Mapped[str] = mapped_column(Text)
status: Mapped[str] = mapped_column(String(50), default='pending')
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
class Promocode(Base):
__tablename__ = 'promocodes'
id: Mapped[int] = mapped_column(primary_key=True)
code: Mapped[str] = mapped_column(String(255), unique=True, index=True)
discount_amount: Mapped[float] = mapped_column(Float)
discount_percent: Mapped[Optional[int]] = mapped_column(Integer)
usage_limit: Mapped[int] = mapped_column(Integer, default=1)
used_count: Mapped[int] = mapped_column(Integer, default=0)
expires_at: Mapped[Optional[datetime]] = mapped_column(DateTime)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
class PromocodeUsage(Base):
__tablename__ = 'promocode_usage'
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(BigInteger, index=True)
promocode_id: Mapped[int] = mapped_column(Integer, index=True)
used_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
class LuckyGame(Base):
__tablename__ = 'lucky_games'
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(BigInteger, index=True)
chosen_number: Mapped[int] = mapped_column(Integer)
winning_numbers: Mapped[str] = mapped_column(String(255))
is_winner: Mapped[bool] = mapped_column(Boolean, default=False)
reward_amount: Mapped[float] = mapped_column(Float, default=0.0)
played_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
class StarPayment(Base):
__tablename__ = 'star_payments'
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(BigInteger, index=True)
stars_amount: Mapped[int] = mapped_column(Integer)
rub_amount: Mapped[float] = mapped_column(Float)
status: Mapped[str] = mapped_column(String(50), default='pending')
telegram_payment_charge_id: Mapped[Optional[str]] = mapped_column(String(255))
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime)
class ServiceRule(Base):
__tablename__ = 'service_rules'
id = Column(Integer, primary_key=True)
title = Column(String(200), nullable=False)
content = Column(Text, nullable=False)
page_order = Column(Integer, nullable=False, default=1)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
class Database:
def __init__(self, database_url: str):
self.engine = create_async_engine(
database_url,
echo=False,
pool_pre_ping=True,
pool_recycle=300
)
self.session_factory = async_sessionmaker(
self.engine,
class_=AsyncSession,
expire_on_commit=False
)
async def init_db(self):
async with self.engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
await self.migrate_user_subscriptions()
await self.migrate_subscription_imported_field()
await self.migrate_referral_tables()
await self.migrate_star_payments_table()
await self.migrate_autopay_fields()
async def toggle_autopay(self, user_subscription_id: int, enabled: bool) -> bool:
"""Включает/выключает автоплатеж для подписки"""
async with self.session_factory() as session:
try:
from sqlalchemy import update
result = await session.execute(
update(UserSubscription)
.where(UserSubscription.id == user_subscription_id)
.values(auto_pay_enabled=enabled)
)
await session.commit()
return result.rowcount > 0
except Exception as e:
logger.error(f"Error toggling autopay: {e}")
await session.rollback()
return False
async def set_autopay_days(self, user_subscription_id: int, days_before: int) -> bool:
"""Устанавливает количество дней до истечения для автоплатежа"""
async with self.session_factory() as session:
try:
from sqlalchemy import update
result = await session.execute(
update(UserSubscription)
.where(UserSubscription.id == user_subscription_id)
.values(auto_pay_days_before=days_before)
)
await session.commit()
return result.rowcount > 0
except Exception as e:
logger.error(f"Error setting autopay days: {e}")
await session.rollback()
return False
async def get_subscriptions_for_autopay(self, days_threshold: int = None) -> List[UserSubscription]:
async with self.session_factory() as session:
try:
from sqlalchemy import select, and_
from datetime import datetime, timedelta
if days_threshold is None:
current_time = datetime.utcnow()
conditions = []
for days in [1, 2, 3, 5, 7]:
threshold_date = current_time + timedelta(days=days)
conditions.append(
and_(
UserSubscription.auto_pay_days_before == days,
UserSubscription.expires_at <= threshold_date,
UserSubscription.expires_at > current_time
)
)
from sqlalchemy import or_
query = select(UserSubscription).where(
and_(
UserSubscription.auto_pay_enabled == True,
UserSubscription.is_active == True,
or_(*conditions)
)
)
else:
threshold_date = datetime.utcnow() + timedelta(days=days_threshold)
query = select(UserSubscription).where(
and_(
UserSubscription.auto_pay_enabled == True,
UserSubscription.is_active == True,
UserSubscription.expires_at <= threshold_date,
UserSubscription.expires_at > datetime.utcnow()
)
)
result = await session.execute(query)
return list(result.scalars().all())
except Exception as e:
logger.error(f"Error getting subscriptions for autopay: {e}")
return []
async def migrate_referral_tables(self):
try:
async with self.engine.begin() as conn:
try:
await conn.execute(text("SELECT 1 FROM referral_programs LIMIT 1"))
logger.info("referral_programs table already exists")
except Exception:
await conn.execute(text("""
CREATE TABLE referral_programs (
id SERIAL PRIMARY KEY,
referrer_id BIGINT NOT NULL,
referred_id BIGINT UNIQUE NOT NULL,
referral_code VARCHAR(20) NOT NULL,
first_reward_paid BOOLEAN DEFAULT FALSE,
total_earned DOUBLE PRECISION DEFAULT 0.0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
first_reward_at TIMESTAMP
)
"""))
try:
await conn.execute(text("CREATE INDEX idx_referrer ON referral_programs(referrer_id)"))
await conn.execute(text("CREATE INDEX idx_referred ON referral_programs(referred_id)"))
await conn.execute(text("CREATE INDEX idx_referral_code ON referral_programs(referral_code)"))
except Exception as e:
logger.warning(f"Some referral_programs indexes may already exist: {e}")
logger.info("Created referral_programs table")
try:
await conn.execute(text("SELECT 1 FROM referral_earnings LIMIT 1"))
logger.info("referral_earnings table already exists")
except Exception:
await conn.execute(text("""
CREATE TABLE referral_earnings (
id SERIAL PRIMARY KEY,
referrer_id BIGINT NOT NULL,
referred_id BIGINT NOT NULL,
amount DOUBLE PRECISION NOT NULL,
earning_type VARCHAR(20) NOT NULL,
related_payment_id INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"""))
try:
await conn.execute(text("CREATE INDEX idx_referrer_earnings ON referral_earnings(referrer_id)"))
await conn.execute(text("CREATE INDEX idx_referred_earnings ON referral_earnings(referred_id)"))
await conn.execute(text("CREATE INDEX idx_earning_type ON referral_earnings(earning_type)"))
except Exception as e:
logger.warning(f"Some referral_earnings indexes may already exist: {e}")
logger.info("Created referral_earnings table")
logger.info("Successfully created referral system tables")
except Exception as e:
logger.error(f"Error creating referral tables: {e}")
async def close(self):
await self.engine.dispose()
async def get_user_by_telegram_id(self, telegram_id: int) -> Optional[User]:
async with self.session_factory() as session:
try:
from sqlalchemy import select
result = await session.execute(
select(User).where(User.telegram_id == telegram_id)
)
return result.scalar_one_or_none()
except Exception as e:
logger.error(f"Error getting user by telegram_id {telegram_id}: {e}")
return None
async def create_user(self, telegram_id: int, username: str = None,
first_name: str = None, last_name: str = None,
language: str = 'ru', is_admin: bool = False) -> User:
async with self.session_factory() as session:
try:
user = User(
telegram_id=telegram_id,
username=username,
first_name=first_name,
last_name=last_name,
language=language,
is_admin=is_admin
)
session.add(user)
await session.commit()
await session.refresh(user)
return user
except Exception as e:
logger.error(f"Error creating user {telegram_id}: {e}")
await session.rollback()
raise
async def update_user(self, user: User) -> User:
async with self.session_factory() as session:
try:
await session.merge(user)
await session.commit()
return user
except Exception as e:
logger.error(f"Error updating user {user.telegram_id}: {e}")
await session.rollback()
raise
async def add_balance(self, user_id: int, amount: float) -> bool:
async with self.session_factory() as session:
try:
from sqlalchemy import select, update
result = await session.execute(
update(User)
.where(User.telegram_id == user_id)
.values(balance=User.balance + amount)
)
await session.commit()
return result.rowcount > 0
except Exception as e:
logger.error(f"Error adding balance to user {user_id}: {e}")
await session.rollback()
return False
async def get_all_subscriptions(self, include_inactive: bool = False, exclude_trial: bool = True, exclude_imported: bool = True) -> List[Subscription]:
async with self.session_factory() as session:
try:
from sqlalchemy import select
query = select(Subscription)
if not include_inactive:
query = query.where(Subscription.is_active == True)
if exclude_trial:
query = query.where(Subscription.is_trial == False)
if exclude_imported:
query = query.where(Subscription.is_imported == False)
result = await session.execute(query)
return list(result.scalars().all())
except Exception as e:
logger.error(f"Error getting subscriptions: {e}")
return []
async def get_all_subscriptions_admin(self) -> List[Subscription]:
async with self.session_factory() as session:
try:
from sqlalchemy import select
result = await session.execute(select(Subscription))
return list(result.scalars().all())
except Exception as e:
logger.error(f"Error getting admin subscriptions: {e}")
return []
async def migrate_subscription_imported_field(self):
try:
async with self.engine.begin() as conn:
try:
await conn.execute(text("SELECT is_imported FROM subscriptions LIMIT 1"))
logger.info("is_imported field already exists")
except Exception:
try:
await conn.execute(text("""
ALTER TABLE subscriptions
ADD COLUMN is_imported BOOLEAN DEFAULT FALSE
"""))
logger.info("Successfully added is_imported field to subscriptions table")
except Exception as e:
logger.warning(f"Error adding is_imported field: {e}")
except Exception as e:
logger.error(f"Error during subscription migration: {e}")
async def get_subscription_by_id(self, subscription_id: int) -> Optional[Subscription]:
async with self.session_factory() as session:
try:
from sqlalchemy import select
result = await session.execute(
select(Subscription).where(Subscription.id == subscription_id)
)
return result.scalar_one_or_none()
except Exception as e:
logger.error(f"Error getting subscription {subscription_id}: {e}")
return None
async def create_subscription(self, name: str, description: str, price: float,
duration_days: int, traffic_limit_gb: int,
squad_uuid: str, is_imported: bool = False) -> Subscription:
async with self.session_factory() as session:
try:
subscription = Subscription(
name=name,
description=description,
price=price,
duration_days=duration_days,
traffic_limit_gb=traffic_limit_gb,
squad_uuid=squad_uuid,
is_imported=is_imported
)
session.add(subscription)
await session.commit()
await session.refresh(subscription)
return subscription
except Exception as e:
logger.error(f"Error creating subscription: {e}")
await session.rollback()
raise
async def update_subscription(self, subscription: Subscription) -> Subscription:
async with self.session_factory() as session:
try:
await session.merge(subscription)
await session.commit()
return subscription
except Exception as e:
logger.error(f"Error updating subscription {subscription.id}: {e}")
await session.rollback()
raise
async def delete_subscription(self, subscription_id: int) -> bool:
async with self.session_factory() as session:
try:
from sqlalchemy import delete
result = await session.execute(
delete(Subscription).where(Subscription.id == subscription_id)
)
await session.commit()
return result.rowcount > 0
except Exception as e:
logger.error(f"Error deleting subscription {subscription_id}: {e}")
await session.rollback()
return False
async def get_user_subscriptions(self, user_id: int) -> List[UserSubscription]:
async with self.session_factory() as session:
try:
from sqlalchemy import select
result = await session.execute(
select(UserSubscription).where(UserSubscription.user_id == user_id)
)
return list(result.scalars().all())
except Exception as e:
logger.error(f"Error getting user subscriptions for {user_id}: {e}")
return []
async def create_user_subscription(self, user_id: int, subscription_id: int,
short_uuid: str, expires_at: datetime,
is_active: bool = True, traffic_limit_gb: int = None) -> Optional[UserSubscription]:
async with self.session_factory() as session:
try:
from sqlalchemy import select
existing = await session.execute(
select(UserSubscription).where(
UserSubscription.user_id == user_id,
UserSubscription.short_uuid == short_uuid
)
)
existing_sub = existing.scalar_one_or_none()
if existing_sub:
logger.warning(f"Subscription with short_uuid {short_uuid} already exists for user {user_id}")
return existing_sub
new_subscription = UserSubscription(
user_id=user_id,
subscription_id=subscription_id,
short_uuid=short_uuid,
expires_at=expires_at,
is_active=is_active
)
session.add(new_subscription)
await session.commit()
await session.refresh(new_subscription)
return new_subscription
except Exception as e:
logger.error(f"Error creating user subscription: {e}")
await session.rollback()
return None
async def create_payment(self, user_id: int, amount: float, payment_type: str,
description: str, status: str = 'pending') -> Payment:
async with self.session_factory() as session:
try:
payment = Payment(
user_id=user_id,
amount=amount,
payment_type=payment_type,
description=description,
status=status
)
session.add(payment)
await session.commit()
await session.refresh(payment)
return payment
except Exception as e:
logger.error(f"Error creating payment: {e}")
await session.rollback()
raise
async def get_payment_by_id(self, payment_id: int) -> Optional[Payment]:
async with self.session_factory() as session:
try:
from sqlalchemy import select
result = await session.execute(
select(Payment).where(Payment.id == payment_id)
)
return result.scalar_one_or_none()
except Exception as e:
logger.error(f"Error getting payment {payment_id}: {e}")
return None
async def update_payment(self, payment: Payment) -> Payment:
async with self.session_factory() as session:
try:
await session.merge(payment)
await session.commit()
return payment
except Exception as e:
logger.error(f"Error updating payment {payment.id}: {e}")
await session.rollback()
raise
async def get_user_payments(self, user_id: int) -> List[Payment]:
async with self.session_factory() as session:
try:
from sqlalchemy import select, desc
result = await session.execute(
select(Payment)
.where(Payment.user_id == user_id)
.order_by(desc(Payment.created_at))
)
return list(result.scalars().all())
except Exception as e:
logger.error(f"Error getting user payments for {user_id}: {e}")
return []
async def get_promocode_by_code(self, code: str) -> Optional[Promocode]:
async with self.session_factory() as session:
try:
from sqlalchemy import select
result = await session.execute(
select(Promocode).where(Promocode.code == code)
)
return result.scalar_one_or_none()
except Exception as e:
logger.error(f"Error getting promocode {code}: {e}")
return None
async def create_promocode(self, code: str, discount_amount: float = 0,
discount_percent: int = None, usage_limit: int = 1,
expires_at: datetime = None) -> Promocode:
async with self.session_factory() as session:
try:
promocode = Promocode(
code=code,
discount_amount=discount_amount,
discount_percent=discount_percent,
usage_limit=usage_limit,
expires_at=expires_at
)
session.add(promocode)
await session.commit()
await session.refresh(promocode)
return promocode
except Exception as e:
logger.error(f"Error creating promocode: {e}")
await session.rollback()
raise
async def use_promocode(self, user_id: int, promocode: Promocode) -> bool:
async with self.session_factory() as session:
try:
from sqlalchemy import select
existing = await session.execute(
select(PromocodeUsage).where(
PromocodeUsage.user_id == user_id,
PromocodeUsage.promocode_id == promocode.id
)
)
if existing.scalar_one_or_none():
return False
usage = PromocodeUsage(user_id=user_id, promocode_id=promocode.id)
session.add(usage)
promocode.used_count += 1
await session.merge(promocode)
await session.commit()
return True
except Exception as e:
logger.error(f"Error using promocode: {e}")
await session.rollback()
return False
async def get_all_promocodes(self) -> List[Promocode]:
async with self.session_factory() as session:
try:
from sqlalchemy import select
result = await session.execute(select(Promocode))
return list(result.scalars().all())
except Exception as e:
logger.error(f"Error getting promocodes: {e}")
return []
async def get_all_users(self) -> List[User]:
async with self.session_factory() as session:
try:
from sqlalchemy import select
result = await session.execute(select(User))
return list(result.scalars().all())
except Exception as e:
logger.error(f"Error getting all users: {e}")
return []
async def get_stats(self) -> dict:
async with self.session_factory() as session:
try:
from sqlalchemy import select, func
total_users = await session.execute(
select(func.count(User.id))
)
total_users = total_users.scalar()
total_subs_non_trial = await session.execute(
select(func.count(UserSubscription.id))
.join(Subscription, UserSubscription.subscription_id == Subscription.id)
.where(Subscription.is_trial == False)
)
total_subs_non_trial = total_subs_non_trial.scalar()
total_payments = await session.execute(
select(func.sum(Payment.amount)).where(
Payment.status == 'completed',
Payment.payment_type != 'trial'
)
)
total_payments = total_payments.scalar() or 0
return {
'total_users': total_users,
'total_subscriptions_non_trial': total_subs_non_trial,
'total_revenue': total_payments
}
except Exception as e:
logger.error(f"Error getting stats: {e}")
return {
'total_users': 0,
'total_subscriptions_non_trial': 0,
'total_revenue': 0
}
async def get_trial_subscriptions(self) -> List[Subscription]:
async with self.session_factory() as session:
try:
from sqlalchemy import select
result = await session.execute(
select(Subscription).where(Subscription.is_trial == True)
)
return list(result.scalars().all())
except Exception as e:
logger.error(f"Error getting trial subscriptions: {e}")
return []
async def get_user_subscription_by_short_uuid(self, user_id: int, short_uuid: str) -> Optional[UserSubscription]:
async with self.session_factory() as session:
try:
from sqlalchemy import select
result = await session.execute(
select(UserSubscription).where(
UserSubscription.user_id == user_id,
UserSubscription.short_uuid == short_uuid
)
)
return result.scalar_one_or_none()
except Exception as e:
logger.error(f"Error getting user subscription by short_uuid: {e}")
return None
async def update_user_subscription(self, user_subscription: UserSubscription) -> bool:
async with self.session_factory() as session:
try:
user_subscription.updated_at = datetime.utcnow()
await session.merge(user_subscription)
await session.commit()
return True
except Exception as e:
logger.error(f"Error updating user subscription: {e}")
await session.rollback()
return False
async def migrate_user_subscriptions(self):
try:
async with self.engine.begin() as conn:
fields_to_add = [
("traffic_limit_gb", "INTEGER"),
("updated_at", "TIMESTAMP")
]
for field_name, field_type in fields_to_add:
try:
await conn.execute(text(f"SELECT {field_name} FROM user_subscriptions LIMIT 1"))
except Exception:
try:
await conn.execute(text(f"""
ALTER TABLE user_subscriptions
ADD COLUMN {field_name} {field_type}
"""))
logger.info(f"Added {field_name} column to user_subscriptions")
except Exception as e:
logger.warning(f"Error adding {field_name} column: {e}")
logger.info("Successfully migrated user_subscriptions table")
except Exception as e:
logger.error(f"Error during user_subscriptions migration: {e}")
async def get_expiring_subscriptions(self, user_id: int, days_threshold: int = 3) -> List[UserSubscription]:
async with self.session_factory() as session:
try:
from sqlalchemy import select
from datetime import datetime, timedelta
threshold_date = datetime.utcnow() + timedelta(days=days_threshold)
result = await session.execute(
select(UserSubscription).where(
UserSubscription.user_id == user_id,
UserSubscription.is_active == True,
UserSubscription.expires_at <= threshold_date,
UserSubscription.expires_at > datetime.utcnow()
)
)
return list(result.scalars().all())
except Exception as e:
logger.error(f"Error getting expiring subscriptions for {user_id}: {e}")
return []
async def has_used_trial(self, user_id: int) -> bool:
async with self.session_factory() as session:
try:
from sqlalchemy import select
result = await session.execute(
select(User.is_trial_used).where(User.telegram_id == user_id)
)
is_trial_used = result.scalar_one_or_none()
return is_trial_used or False
except Exception as e:
logger.error(f"Error checking trial usage for user {user_id}: {e}")
return False
async def mark_trial_used(self, user_id: int) -> bool:
async with self.session_factory() as session:
try:
from sqlalchemy import update
result = await session.execute(
update(User)
.where(User.telegram_id == user_id)
.values(is_trial_used=True)
)
await session.commit()
return result.rowcount > 0
except Exception as e:
logger.error(f"Error marking trial used for user {user_id}: {e}")
await session.rollback()
return False
async def get_all_payments_paginated(self, offset: int = 0, limit: int = 10) -> tuple[List[Payment], int]:
async with self.session_factory() as session:
try:
from sqlalchemy import select, desc, func
count_result = await session.execute(
select(func.count(Payment.id))
)
total_count = count_result.scalar()
result = await session.execute(
select(Payment)
.order_by(desc(Payment.created_at))
.offset(offset)
.limit(limit)
)
payments = list(result.scalars().all())
return payments, total_count
except Exception as e:
logger.error(f"Error getting paginated payments: {e}")
return [], 0
async def get_payments_by_type_paginated(self, payment_type: str, offset: int = 0, limit: int = 10) -> tuple[List[Payment], int]:
async with self.session_factory() as session:
try:
from sqlalchemy import select, desc, func
count_result = await session.execute(
select(func.count(Payment.id)).where(Payment.payment_type == payment_type)
)
total_count = count_result.scalar()
result = await session.execute(
select(Payment)
.where(Payment.payment_type == payment_type)
.order_by(desc(Payment.created_at))
.offset(offset)
.limit(limit)
)
payments = list(result.scalars().all())
return payments, total_count
except Exception as e:
logger.error(f"Error getting paginated payments by type: {e}")
return [], 0
async def get_payments_by_status_paginated(self, status: str, offset: int = 0, limit: int = 10) -> tuple[List[Payment], int]:
async with self.session_factory() as session:
try:
from sqlalchemy import select, desc, func
count_result = await session.execute(
select(func.count(Payment.id)).where(Payment.status == status)
)
total_count = count_result.scalar()
result = await session.execute(
select(Payment)
.where(Payment.status == status)
.order_by(desc(Payment.created_at))
.offset(offset)
.limit(limit)
)
payments = list(result.scalars().all())
return payments, total_count
except Exception as e:
logger.error(f"Error getting paginated payments by status: {e}")
return [], 0
async def get_user_subscriptions_by_plan_id(self, plan_id: int) -> List[UserSubscription]:
async with self.session_factory() as session:
try:
from sqlalchemy import select
result = await session.execute(
select(UserSubscription).where(UserSubscription.subscription_id == plan_id)
)
return list(result.scalars().all())
except Exception as e:
logger.error(f"Error getting user subscriptions for plan {plan_id}: {e}")
return []
async def delete_user_subscription(self, user_subscription_id: int) -> bool:
async with self.session_factory() as session:
try:
from sqlalchemy import delete
result = await session.execute(
delete(UserSubscription).where(UserSubscription.id == user_subscription_id)
)
await session.commit()
return result.rowcount > 0
except Exception as e:
logger.error(f"Error deleting user subscription {user_subscription_id}: {e}")
await session.rollback()
return False
async def create_referral(self, referrer_id: int, referred_id: int, referral_code: str) -> Optional[ReferralProgram]:
async with self.session_factory() as session:
try:
from sqlalchemy import select
if referred_id == 0:
placeholder_id = 999999999 - referrer_id
existing = await session.execute(
select(ReferralProgram).where(
ReferralProgram.referrer_id == referrer_id,
ReferralProgram.referred_id == placeholder_id
)
)
existing_referral = existing.scalar_one_or_none()
if existing_referral:
logger.info(f"Referral code already exists for user {referrer_id}")
return existing_referral
referral = ReferralProgram(
referrer_id=referrer_id,
referred_id=placeholder_id,
referral_code=referral_code
)
session.add(referral)
await session.commit()
await session.refresh(referral)
logger.info(f"Created referral code storage for user {referrer_id}")
return referral
existing = await session.execute(
select(ReferralProgram).where(
ReferralProgram.referred_id == referred_id,
ReferralProgram.referred_id < 900000000,
ReferralProgram.referred_id > 0
)
)
if existing.scalar_one_or_none():
logger.info(f"User {referred_id} already has a real referrer")
return None
if referrer_id == referred_id:
logger.warning(f"User {referrer_id} tried to refer themselves")
return None
referral = ReferralProgram(
referrer_id=referrer_id,
referred_id=referred_id,
referral_code=referral_code
)
session.add(referral)
await session.commit()
await session.refresh(referral)
logger.info(f"Created real referral: {referrer_id} -> {referred_id}")
return referral
except Exception as e:
logger.error(f"Error creating referral: {e}")
await session.rollback()
return None
async def get_referral_by_referred_id(self, referred_id: int) -> Optional[ReferralProgram]:
async with self.session_factory() as session:
try:
from sqlalchemy import select
result = await session.execute(
select(ReferralProgram).where(ReferralProgram.referred_id == referred_id)
)
return result.scalar_one_or_none()
except Exception as e:
logger.error(f"Error getting referral: {e}")
return None
async def get_user_referrals(self, referrer_id: int) -> List[ReferralProgram]:
async with self.session_factory() as session:
try:
from sqlalchemy import select, and_
result = await session.execute(
select(ReferralProgram).where(
and_(
ReferralProgram.referrer_id == referrer_id,
ReferralProgram.referred_id < 900000000,
ReferralProgram.referred_id > 0
)
)
)
referrals = list(result.scalars().all())
logger.debug(f"Found {len(referrals)} real referrals for user {referrer_id}")
return referrals
except Exception as e:
logger.error(f"Error getting user referrals: {e}")
return []
async def create_referral(self, referrer_id: int, referred_id: int, referral_code: str) -> Optional[ReferralProgram]:
async with self.session_factory() as session:
try:
from sqlalchemy import select
if referred_id == 0:
placeholder_id = 999999999 - referrer_id
existing = await session.execute(
select(ReferralProgram).where(
ReferralProgram.referrer_id == referrer_id,
ReferralProgram.referred_id == placeholder_id
)
)
existing_referral = existing.scalar_one_or_none()
if existing_referral:
logger.info(f"Referral code already exists for user {referrer_id}")
return existing_referral
referral = ReferralProgram(
referrer_id=referrer_id,
referred_id=placeholder_id,
referral_code=referral_code
)
session.add(referral)
await session.commit()
await session.refresh(referral)
return referral
existing = await session.execute(
select(ReferralProgram).where(ReferralProgram.referred_id == referred_id)
)
if existing.scalar_one_or_none():
logger.info(f"User {referred_id} already has a referrer")
return None
if referrer_id == referred_id:
logger.warning(f"User {referrer_id} tried to refer themselves")
return None
referral = ReferralProgram(
referrer_id=referrer_id,
referred_id=referred_id,
referral_code=referral_code
)
session.add(referral)
await session.commit()
await session.refresh(referral)
return referral
except Exception as e:
logger.error(f"Error creating referral: {e}")
await session.rollback()
return None
async def get_user_referral_stats(self, user_id: int) -> Dict:
async with self.session_factory() as session:
try:
from sqlalchemy import select, func, and_, or_
placeholder_id = 999999999 - user_id
referrals_count = await session.execute(
select(func.count(ReferralProgram.id))
.where(
and_(
ReferralProgram.referrer_id == user_id,
ReferralProgram.referred_id != placeholder_id,
ReferralProgram.referred_id != 0
)
)
)
active_referrals = await session.execute(
select(func.count(ReferralProgram.id))
.where(
and_(
ReferralProgram.referrer_id == user_id,
ReferralProgram.first_reward_paid == True,
ReferralProgram.referred_id != placeholder_id,
ReferralProgram.referred_id != 0
)
)
)
total_earned = await session.execute(
select(func.sum(ReferralEarning.amount))
.where(ReferralEarning.referrer_id == user_id)
)
result = {
'total_referrals': referrals_count.scalar() or 0,
'active_referrals': active_referrals.scalar() or 0,
'total_earned': total_earned.scalar() or 0.0
}
logger.info(f"Referral stats for user {user_id}: {result}")
return result
except Exception as e:
logger.error(f"Error getting referral stats: {e}")
return {
'total_referrals': 0,
'active_referrals': 0,
'total_earned': 0.0
}
async def generate_unique_referral_code(self, user_id: int) -> str:
async with self.session_factory() as session:
try:
import secrets
import string
base_code = f"REF{user_id}"
from sqlalchemy import select
existing = await session.execute(
select(ReferralProgram).where(ReferralProgram.referral_code == base_code)
)
if not existing.scalar_one_or_none():
return base_code
for _ in range(10):
random_suffix = ''.join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(4))
code = f"REF{user_id}{random_suffix}"
existing = await session.execute(
select(ReferralProgram).where(ReferralProgram.referral_code == code)
)
if not existing.scalar_one_or_none():
return code
return f"REF{''.join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(8))}"
except Exception as e:
logger.error(f"Error generating referral code: {e}")
return f"REF{user_id}ERR"
async def get_user_referrals(self, referrer_id: int) -> List[ReferralProgram]:
async with self.session_factory() as session:
try:
from sqlalchemy import select, and_
placeholder_id = 999999999 - referrer_id
result = await session.execute(
select(ReferralProgram).where(
and_(
ReferralProgram.referrer_id == referrer_id,
ReferralProgram.referred_id != placeholder_id,
ReferralProgram.referred_id != 0
)
).order_by(ReferralProgram.created_at.desc())
)
referrals = list(result.scalars().all())
logger.info(f"Found {len(referrals)} real referrals for user {referrer_id} (excluding placeholder {placeholder_id})")
for ref in referrals:
logger.debug(f"Referral: referrer={ref.referrer_id}, referred={ref.referred_id}, "
f"first_reward_paid={ref.first_reward_paid}, total_earned={ref.total_earned}")
return referrals
except Exception as e:
logger.error(f"Error getting user referrals: {e}")
return []
async def create_referral_earning(self, referrer_id: int, referred_id: int,
amount: float, earning_type: str,
related_payment_id: Optional[int] = None) -> bool:
async with self.session_factory() as session:
try:
earning = ReferralEarning(
referrer_id=referrer_id,
referred_id=referred_id,
amount=amount,
earning_type=earning_type,
related_payment_id=related_payment_id
)
session.add(earning)
from sqlalchemy import select, update
referral = await session.execute(
select(ReferralProgram).where(
ReferralProgram.referrer_id == referrer_id,
ReferralProgram.referred_id == referred_id
)
)
referral_record = referral.scalar_one_or_none()
if referral_record:
referral_record.total_earned += amount
if earning_type == 'first_reward':
referral_record.first_reward_paid = True
referral_record.first_reward_at = datetime.utcnow()
await session.merge(referral_record)
await session.commit()
return True
except Exception as e:
logger.error(f"Error creating referral earning: {e}")
await session.rollback()
return False
async def get_promocode_by_id(self, promocode_id: int) -> Optional[Promocode]:
async with self.session_factory() as session:
try:
from sqlalchemy import select
result = await session.execute(
select(Promocode).where(Promocode.id == promocode_id)
)
return result.scalar_one_or_none()
except Exception as e:
logger.error(f"Error getting promocode by ID {promocode_id}: {e}")
return None
async def update_promocode(self, promocode: Promocode) -> Promocode:
async with self.session_factory() as session:
try:
await session.merge(promocode)
await session.commit()
return promocode
except Exception as e:
logger.error(f"Error updating promocode {promocode.id}: {e}")
await session.rollback()
raise
async def delete_promocode(self, promocode_id: int) -> bool:
async with self.session_factory() as session:
try:
from sqlalchemy import delete
await session.execute(
delete(PromocodeUsage).where(PromocodeUsage.promocode_id == promocode_id)
)
result = await session.execute(
delete(Promocode).where(Promocode.id == promocode_id)
)
await session.commit()
return result.rowcount > 0
except Exception as e:
logger.error(f"Error deleting promocode {promocode_id}: {e}")
await session.rollback()
return False
async def get_regular_promocodes(self, include_inactive: bool = True) -> List[Promocode]:
async with self.session_factory() as session:
try:
from sqlalchemy import select, and_
query = select(Promocode).where(~Promocode.code.startswith('REF'))
if not include_inactive:
query = query.where(Promocode.is_active == True)
result = await session.execute(query.order_by(Promocode.created_at.desc()))
return list(result.scalars().all())
except Exception as e:
logger.error(f"Error getting regular promocodes: {e}")
return []
async def get_expired_promocodes(self) -> List[Promocode]:
async with self.session_factory() as session:
try:
from sqlalchemy import select, and_
from datetime import datetime
result = await session.execute(
select(Promocode).where(
and_(
Promocode.expires_at < datetime.utcnow(),
~Promocode.code.startswith('REF')
)
)
)
return list(result.scalars().all())
except Exception as e:
logger.error(f"Error getting expired promocodes: {e}")
return []
async def cleanup_expired_promocodes(self) -> int:
async with self.session_factory() as session:
try:
from sqlalchemy import delete, and_
from datetime import datetime
expired_promos = await session.execute(
select(Promocode.id).where(
and_(
Promocode.expires_at < datetime.utcnow(),
~Promocode.code.startswith('REF')
)
)
)
expired_ids = [row[0] for row in expired_promos.fetchall()]
if not expired_ids:
return 0
await session.execute(
delete(PromocodeUsage).where(PromocodeUsage.promocode_id.in_(expired_ids))
)
result = await session.execute(
delete(Promocode).where(Promocode.id.in_(expired_ids))
)
await session.commit()
return result.rowcount
except Exception as e:
logger.error(f"Error cleaning up expired promocodes: {e}")
await session.rollback()
return 0
async def deactivate_all_regular_promocodes(self) -> int:
async with self.session_factory() as session:
try:
from sqlalchemy import update, and_
result = await session.execute(
update(Promocode)
.where(
and_(
~Promocode.code.startswith('REF'),
Promocode.is_active == True
)
)
.values(is_active=False)
)
await session.commit()
return result.rowcount
except Exception as e:
logger.error(f"Error deactivating all promocodes: {e}")
await session.rollback()
return 0
async def get_promocode_stats(self) -> Dict:
async with self.session_factory() as session:
try:
from sqlalchemy import select, func, and_
from datetime import datetime
total_promos = await session.execute(
select(func.count(Promocode.id)).where(~Promocode.code.startswith('REF'))
)
total_count = total_promos.scalar() or 0
active_promos = await session.execute(
select(func.count(Promocode.id)).where(
and_(
~Promocode.code.startswith('REF'),
Promocode.is_active == True
)
)
)
active_count = active_promos.scalar() or 0
expired_promos = await session.execute(
select(func.count(Promocode.id)).where(
and_(
~Promocode.code.startswith('REF'),
Promocode.expires_at < datetime.utcnow()
)
)
)
expired_count = expired_promos.scalar() or 0
total_usage = await session.execute(
select(func.sum(Promocode.used_count)).where(~Promocode.code.startswith('REF'))
)
usage_count = total_usage.scalar() or 0
total_discount = await session.execute(
select(func.sum(Promocode.discount_amount * Promocode.used_count)).where(
~Promocode.code.startswith('REF')
)
)
discount_amount = total_discount.scalar() or 0.0
top_promos = await session.execute(
select(Promocode.code, Promocode.used_count, Promocode.discount_amount)
.where(~Promocode.code.startswith('REF'))
.order_by(Promocode.used_count.desc())
.limit(5)
)
top_promocodes = list(top_promos.fetchall())
return {
'total_promocodes': total_count,
'active_promocodes': active_count,
'expired_promocodes': expired_count,
'total_usage': usage_count,
'total_discount_amount': discount_amount,
'top_promocodes': top_promocodes
}
except Exception as e:
logger.error(f"Error getting promocode stats: {e}")
return {
'total_promocodes': 0,
'active_promocodes': 0,
'expired_promocodes': 0,
'total_usage': 0,
'total_discount_amount': 0.0,
'top_promocodes': []
}
async def get_promocode_usage_by_id(self, promocode_id: int) -> List[PromocodeUsage]:
async with self.session_factory() as session:
try:
from sqlalchemy import select
result = await session.execute(
select(PromocodeUsage)
.where(PromocodeUsage.promocode_id == promocode_id)
.order_by(PromocodeUsage.used_at.desc())
)
return list(result.scalars().all())
except Exception as e:
logger.error(f"Error getting promocode usage for {promocode_id}: {e}")
return []
async def create_lucky_game(self, user_id: int, chosen_number: int,
winning_numbers: List[int], is_winner: bool,
reward_amount: float) -> Optional['LuckyGame']:
async with self.session_factory() as session:
try:
game = LuckyGame(
user_id=user_id,
chosen_number=chosen_number,
winning_numbers=winning_numbers,
is_winner=is_winner,
reward_amount=reward_amount
)
session.add(game)
await session.commit()
await session.refresh(game)
logger.info(f"Lucky game created: user_id={user_id}, chosen_number={chosen_number}, is_winner={is_winner}")
return game
except Exception as e:
await session.rollback()
logger.error(f"Error creating lucky game: {e}")
return None
async def get_user_last_game_today(self, user_id: int) -> Optional[LuckyGame]:
async with self.session_factory() as session:
try:
from sqlalchemy import select, and_
from datetime import date
today = date.today()
result = await session.execute(
select(LuckyGame)
.where(
and_(
LuckyGame.user_id == user_id,
LuckyGame.played_at >= datetime.combine(today, datetime.min.time()),
LuckyGame.played_at < datetime.combine(today + timedelta(days=1), datetime.min.time())
)
)
.order_by(LuckyGame.played_at.desc())
.limit(1)
)
return result.scalar_one_or_none()
except Exception as e:
logger.error(f"Error getting user last game today: {e}")
return None
async def get_user_game_stats(self, user_id: int) -> Dict[str, Any]:
async with self.session_factory() as session:
try:
from sqlalchemy import select, func, and_
games_count = await session.execute(
select(func.count(LuckyGame.id))
.where(LuckyGame.user_id == user_id)
)
total_games = games_count.scalar() or 0
wins_count = await session.execute(
select(func.count(LuckyGame.id))
.where(
and_(
LuckyGame.user_id == user_id,
LuckyGame.is_winner == True
)
)
)
total_wins = wins_count.scalar() or 0
total_reward = await session.execute(
select(func.sum(LuckyGame.reward_amount))
.where(LuckyGame.user_id == user_id)
)
total_won = total_reward.scalar() or 0.0
return {
'total_games': total_games,
'total_wins': total_wins,
'total_won': total_won,
'win_rate': (total_wins / total_games * 100) if total_games > 0 else 0
}
except Exception as e:
logger.error(f"Error getting user game stats: {e}")
return {
'total_games': 0,
'total_wins': 0,
'total_won': 0.0,
'win_rate': 0
}
async def get_user_game_history(self, user_id: int, limit: int = 10) -> List[Dict[str, Any]]:
async with self.session_factory() as session:
try:
from sqlalchemy import select
import json
result = await session.execute(
select(LuckyGame)
.where(LuckyGame.user_id == user_id)
.order_by(LuckyGame.played_at.desc())
.limit(limit)
)
games = result.scalars().all()
history = []
for game in games:
winning_numbers = json.loads(game.winning_numbers) if game.winning_numbers else []
history.append({
'id': game.id,
'chosen_number': game.chosen_number,
'winning_numbers': winning_numbers,
'is_winner': game.is_winner,
'reward_amount': game.reward_amount,
'played_at': game.played_at
})
return history
except Exception as e:
logger.error(f"Error getting user game history: {e}")
return []
async def can_play_lucky_game_today(self, user_id: int) -> bool:
try:
last_game = await self.get_user_last_game_today(user_id)
return last_game is None
except Exception as e:
logger.error(f"Error checking can play today: {e}")
return True
async def create_star_payment(self, user_id: int, stars_amount: int, rub_amount: float) -> StarPayment:
"""Создать платеж через звезды"""
async with self.session_factory() as session:
try:
payment = StarPayment(
user_id=user_id,
stars_amount=stars_amount,
rub_amount=rub_amount,
status='pending'
)
session.add(payment)
await session.commit()
await session.refresh(payment)
return payment
except Exception as e:
logger.error(f"Error creating star payment: {e}")
await session.rollback()
raise
async def get_star_payment_by_id(self, payment_id: int) -> Optional[StarPayment]:
"""Получить платеж через звезды по ID"""
async with self.session_factory() as session:
try:
from sqlalchemy import select
result = await session.execute(
select(StarPayment).where(StarPayment.id == payment_id)
)
return result.scalar_one_or_none()
except Exception as e:
logger.error(f"Error getting star payment {payment_id}: {e}")
return None
async def complete_star_payment(self, payment_id: int, telegram_payment_charge_id: str) -> bool:
"""Завершить платеж через звезды"""
async with self.session_factory() as session:
try:
from sqlalchemy import update
result = await session.execute(
update(StarPayment)
.where(StarPayment.id == payment_id)
.values(
status='completed',
telegram_payment_charge_id=telegram_payment_charge_id,
completed_at=datetime.utcnow()
)
)
if result.rowcount == 0:
return False
payment_result = await session.execute(
select(StarPayment).where(StarPayment.id == payment_id)
)
payment = payment_result.scalar_one_or_none()
if not payment:
return False
balance_result = await session.execute(
update(User)
.where(User.telegram_id == payment.user_id)
.values(balance=User.balance + payment.rub_amount)
)
regular_payment = Payment(
user_id=payment.user_id,
amount=payment.rub_amount,
payment_type='stars',
description=f'Пополнение через Telegram Stars ({payment.stars_amount} ⭐)',
status='completed'
)
session.add(regular_payment)
await session.commit()
return True
except Exception as e:
logger.error(f"Error completing star payment {payment_id}: {e}")
await session.rollback()
return False
async def cancel_star_payment(self, payment_id: int) -> bool:
"""Отменить платеж через звезды"""
async with self.session_factory() as session:
try:
from sqlalchemy import update
result = await session.execute(
update(StarPayment)
.where(StarPayment.id == payment_id)
.values(status='cancelled')
)
await session.commit()
return result.rowcount > 0
except Exception as e:
logger.error(f"Error cancelling star payment {payment_id}: {e}")
await session.rollback()
return False
async def get_user_star_payments(self, user_id: int, limit: int = 10) -> List[StarPayment]:
"""Получить платежи пользователя через звезды"""
async with self.session_factory() as session:
try:
from sqlalchemy import select, desc
result = await session.execute(
select(StarPayment)
.where(StarPayment.user_id == user_id)
.order_by(desc(StarPayment.created_at))
.limit(limit)
)
return list(result.scalars().all())
except Exception as e:
logger.error(f"Error getting user star payments for {user_id}: {e}")
return []
async def migrate_star_payments_table(self):
try:
async with self.engine.begin() as conn:
try:
await conn.execute(text("SELECT 1 FROM star_payments LIMIT 1"))
logger.info("star_payments table already exists")
return
except Exception:
pass
await conn.execute(text("""
CREATE TABLE star_payments (
id SERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
stars_amount INTEGER NOT NULL,
rub_amount DOUBLE PRECISION NOT NULL,
status VARCHAR(50) DEFAULT 'pending',
telegram_payment_charge_id VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP
)
"""))
try:
await conn.execute(text("CREATE INDEX idx_star_payments_user ON star_payments(user_id)"))
await conn.execute(text("CREATE INDEX idx_star_payments_status ON star_payments(status)"))
except Exception as e:
logger.warning(f"Some indexes may already exist: {e}")
logger.info("Successfully created star_payments table")
except Exception as e:
logger.error(f"Error creating star_payments table: {e}")
async def create_service_rule(self, title: str, content: str, page_order: int = None) -> ServiceRule:
async with self.session_factory() as session:
if page_order is None:
result = await session.execute(
select(func.max(ServiceRule.page_order)).where(ServiceRule.is_active == True)
)
max_order = result.scalar() or 0
page_order = max_order + 1
rule = ServiceRule(
title=title,
content=content,
page_order=page_order
)
session.add(rule)
await session.commit()
await session.refresh(rule)
return rule
async def get_all_service_rules(self, active_only: bool = True) -> List[ServiceRule]:
async with self.session_factory() as session:
query = select(ServiceRule).order_by(ServiceRule.page_order)
if active_only:
query = query.where(ServiceRule.is_active == True)
result = await session.execute(query)
return result.scalars().all()
async def get_service_rule_by_id(self, rule_id: int) -> Optional[ServiceRule]:
async with self.session_factory() as session:
result = await session.execute(
select(ServiceRule).where(ServiceRule.id == rule_id)
)
return result.scalar_one_or_none()
async def update_service_rule(self, rule: ServiceRule) -> bool:
try:
async with self.session_factory() as session:
await session.merge(rule)
await session.commit()
return True
except Exception as e:
logger.error(f"Error updating service rule: {e}")
return False
async def delete_service_rule(self, rule_id: int) -> bool:
try:
async with self.session_factory() as session:
result = await session.execute(
select(ServiceRule).where(ServiceRule.id == rule_id)
)
rule = result.scalar_one_or_none()
if rule:
await session.delete(rule)
await session.commit()
return True
return False
except Exception as e:
logger.error(f"Error deleting service rule: {e}")
return False
async def reorder_service_rules(self, rule_orders: List[tuple]) -> bool:
try:
async with self.session_factory() as session:
for rule_id, new_order in rule_orders:
await session.execute(
update(ServiceRule).where(ServiceRule.id == rule_id).values(page_order=new_order)
)
await session.commit()
return True
except Exception as e:
logger.error(f"Error reordering service rules: {e}")
return False
async def migrate_service_rules_table(self):
try:
async with self.engine.begin() as conn:
try:
await conn.execute(text("SELECT 1 FROM service_rules LIMIT 1"))
logger.info("service_rules table already exists")
return
except Exception:
pass
await conn.execute(text("""
CREATE TABLE service_rules (
id SERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL,
content TEXT NOT NULL,
page_order INTEGER NOT NULL DEFAULT 1,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"""))
try:
await conn.execute(text("CREATE INDEX idx_service_rules_order ON service_rules(page_order)"))
await conn.execute(text("CREATE INDEX idx_service_rules_active ON service_rules(is_active)"))
except Exception as e:
logger.warning(f"Some indexes may already exist: {e}")
check_result = await conn.execute(text("SELECT COUNT(*) FROM service_rules"))
count = check_result.scalar()
if count == 0:
await conn.execute(text("""
INSERT INTO service_rules (title, content, page_order) VALUES
('Общие положения',
'**1. Общие положения**
Настоящие Правила определяют условия использования VPN-сервиса.
**1.1** Используя наш сервис, вы соглашаетесь с данными правилами.
**1.2** Мы оставляем за собой право изменять правила в любое время.
**1.3** Продолжение использования сервиса после изменений означает ваше согласие с новыми условиями.',
1),
('Права и обязанности',
'**2. Права и обязанности пользователей**
**2.1 Права пользователя:**
• Использовать VPN-сервис в соответствии с тарифным планом
• Получать техническую поддержку
• Защиту персональных данных
**2.2 Обязанности пользователя:**
Не использовать сервис для незаконной деятельности
Не передавать данные доступа третьим лицам
• Своевременно оплачивать услуги
**2.3 Запрещается:**
• Попытки взлома или нарушения работы сервиса
• Спам и рассылка нежелательных сообщений
• Нарушение авторских прав',
2),
('Оплата и возврат средств',
'**3. Условия оплаты и возврата**
**3.1 Оплата:**
Все платежи производятся в российских рублях
• Доступны различные способы оплаты
• Средства зачисляются автоматически или в течение 24 часов
**3.2 Возврат средств:**
• Возврат возможен в течение 7 дней с момента оплаты
• При технических проблемах возврат производится полностью
• Обращайтесь в поддержку для возврата
**3.3 Скидки и промокоды:**
• Действуют ограничения по времени и количеству использований
• Нельзя комбинировать несколько скидок',
3)
"""))
logger.info("Inserted default service rules")
logger.info("Successfully created service_rules table")
except Exception as e:
logger.error(f"Error creating service_rules table: {e}")
async def migrate_autopay_fields(self):
try:
async with self.engine.begin() as conn:
try:
await conn.execute(text("SELECT auto_pay_enabled FROM user_subscriptions LIMIT 1"))
logger.info("auto_pay_enabled field already exists")
except Exception:
try:
await conn.execute(text("""
ALTER TABLE user_subscriptions
ADD COLUMN auto_pay_enabled BOOLEAN DEFAULT FALSE
"""))
logger.info("Added auto_pay_enabled column")
except Exception as e:
logger.error(f"Error adding auto_pay_enabled column: {e}")
pass
async with self.engine.begin() as conn:
try:
await conn.execute(text("SELECT auto_pay_days_before FROM user_subscriptions LIMIT 1"))
logger.info("auto_pay_days_before field already exists")
except Exception:
try:
await conn.execute(text("""
ALTER TABLE user_subscriptions
ADD COLUMN auto_pay_days_before INTEGER DEFAULT 3
"""))
logger.info("Added auto_pay_days_before column")
except Exception as e:
logger.error(f"Error adding auto_pay_days_before column: {e}")
pass
logger.info("Successfully completed autopay fields migration")
except Exception as e:
logger.error(f"Error during autopay migration: {e}")
async def get_autopay_history(self, limit: int = 50) -> List[Dict[str, Any]]:
async with self.session_factory() as session:
try:
from sqlalchemy import select, desc
result = await session.execute(
select(Payment)
.where(Payment.payment_type == 'autopay')
.order_by(desc(Payment.created_at))
.limit(limit)
)
payments = result.scalars().all()
autopay_history = []
for payment in payments:
try:
user_result = await session.execute(
select(User).where(User.telegram_id == payment.user_id)
)
user_obj = user_result.scalar_one_or_none()
autopay_history.append({
'payment_id': payment.id,
'user_id': payment.user_id,
'username': user_obj.username if user_obj else 'N/A',
'first_name': user_obj.first_name if user_obj else 'N/A',
'amount': payment.amount,
'description': payment.description,
'status': payment.status,
'created_at': payment.created_at
})
except Exception as e:
logger.warning(f"Error processing autopay history for payment {payment.id}: {e}")
continue
return autopay_history
except Exception as e:
logger.error(f"Error getting autopay history: {e}")
return []
async def disable_autopay_for_user(self, user_id: int) -> int:
async with self.session_factory() as session:
try:
from sqlalchemy import update
result = await session.execute(
update(UserSubscription)
.where(
and_(
UserSubscription.user_id == user_id,
UserSubscription.auto_pay_enabled == True
)
)
.values(auto_pay_enabled=False)
)
await session.commit()
return result.rowcount
except Exception as e:
logger.error(f"Error disabling autopay for user {user_id}: {e}")
await session.rollback()
return 0
async def get_autopay_subscription_by_id(self, subscription_id: int) -> Optional[UserSubscription]:
async with self.session_factory() as session:
try:
from sqlalchemy import select
result = await session.execute(
select(UserSubscription).where(
and_(
UserSubscription.id == subscription_id,
UserSubscription.auto_pay_enabled == True
)
)
)
return result.scalar_one_or_none()
except Exception as e:
logger.error(f"Error getting autopay subscription {subscription_id}: {e}")
return None
async def get_autopay_statistics(self) -> Dict[str, Any]:
async with self.session_factory() as session:
try:
from sqlalchemy import select, func, and_, case
from datetime import datetime, timedelta
total_autopay = await session.execute(
select(func.count(UserSubscription.id))
.where(UserSubscription.auto_pay_enabled == True)
)
total_autopay_subscriptions = total_autopay.scalar() or 0
active_autopay = await session.execute(
select(func.count(UserSubscription.id))
.where(
and_(
UserSubscription.auto_pay_enabled == True,
UserSubscription.is_active == True,
UserSubscription.expires_at > datetime.utcnow()
)
)
)
active_autopay_subscriptions = active_autopay.scalar() or 0
expired_autopay = await session.execute(
select(func.count(UserSubscription.id))
.where(
and_(
UserSubscription.auto_pay_enabled == True,
UserSubscription.expires_at <= datetime.utcnow()
)
)
)
expired_autopay_subscriptions = expired_autopay.scalar() or 0
ready_for_autopay = []
current_time = datetime.utcnow()
for days in [1, 2, 3, 5, 7]:
threshold_date = current_time + timedelta(days=days)
ready_count = await session.execute(
select(func.count(UserSubscription.id))
.where(
and_(
UserSubscription.auto_pay_enabled == True,
UserSubscription.is_active == True,
UserSubscription.auto_pay_days_before == days,
UserSubscription.expires_at <= threshold_date,
UserSubscription.expires_at > current_time
)
)
)
count = ready_count.scalar() or 0
ready_for_autopay.append({
'days': days,
'count': count
})
return {
'total_autopay_subscriptions': total_autopay_subscriptions,
'active_autopay_subscriptions': active_autopay_subscriptions,
'expired_autopay_subscriptions': expired_autopay_subscriptions,
'ready_for_autopay': ready_for_autopay
}
except Exception as e:
logger.error(f"Error getting autopay statistics: {e}")
return {
'total_autopay_subscriptions': 0,
'active_autopay_subscriptions': 0,
'expired_autopay_subscriptions': 0,
'ready_for_autopay': []
}
async def get_users_with_insufficient_autopay_balance(self) -> List[Dict[str, Any]]:
async with self.session_factory() as session:
try:
from sqlalchemy import select, and_
from datetime import datetime, timedelta
current_time = datetime.utcnow()
insufficient_users = []
autopay_subs = await session.execute(
select(UserSubscription)
.where(
and_(
UserSubscription.auto_pay_enabled == True,
UserSubscription.is_active == True,
UserSubscription.expires_at > current_time
)
)
)
for user_sub in autopay_subs.scalars().all():
try:
threshold_date = current_time + timedelta(days=user_sub.auto_pay_days_before)
if user_sub.expires_at <= threshold_date:
user_result = await session.execute(
select(User).where(User.telegram_id == user_sub.user_id)
)
user_obj = user_result.scalar_one_or_none()
if not user_obj:
continue
sub_result = await session.execute(
select(Subscription).where(Subscription.id == user_sub.subscription_id)
)
subscription = sub_result.scalar_one_or_none()
if not subscription:
continue
if user_obj.balance < subscription.price:
days_left = (user_sub.expires_at - current_time).days
needed_amount = subscription.price - user_obj.balance
insufficient_users.append({
'user_id': user_obj.telegram_id,
'username': user_obj.username or 'N/A',
'first_name': user_obj.first_name or 'N/A',
'current_balance': user_obj.balance,
'subscription_price': subscription.price,
'needed_amount': needed_amount,
'subscription_name': subscription.name,
'expires_in_days': days_left,
'auto_pay_days_before': user_sub.auto_pay_days_before
})
except Exception as e:
logger.warning(f"Error processing user subscription {user_sub.id}: {e}")
continue
return insufficient_users
except Exception as e:
logger.error(f"Error getting users with insufficient autopay balance: {e}")
return []
async def get_all_user_subscriptions_admin(self, offset: int = 0, limit: int = 20,
filter_type: str = "all") -> tuple[List[Dict], int]:
async with self.session_factory() as session:
try:
from sqlalchemy import select, func, desc, and_, or_
from datetime import datetime, timedelta
base_query = select(
UserSubscription,
Subscription.name.label('subscription_name'),
Subscription.price.label('subscription_price'),
Subscription.is_trial.label('is_trial'),
Subscription.is_imported.label('is_imported'),
User.username.label('user_username'),
User.first_name.label('user_first_name'),
User.telegram_id.label('user_telegram_id')
).select_from(
UserSubscription.__table__.join(
Subscription.__table__, UserSubscription.subscription_id == Subscription.id
).join(
User.__table__, UserSubscription.user_id == User.telegram_id
)
)
current_time = datetime.utcnow()
if filter_type == "active":
base_query = base_query.where(
and_(
UserSubscription.is_active == True,
UserSubscription.expires_at > current_time
)
)
elif filter_type == "expired":
base_query = base_query.where(
or_(
UserSubscription.is_active == False,
UserSubscription.expires_at <= current_time
)
)
elif filter_type == "expiring":
expiring_date = current_time + timedelta(days=7)
base_query = base_query.where(
and_(
UserSubscription.is_active == True,
UserSubscription.expires_at > current_time,
UserSubscription.expires_at <= expiring_date
)
)
elif filter_type == "autopay":
base_query = base_query.where(UserSubscription.auto_pay_enabled == True)
elif filter_type == "trial":
base_query = base_query.where(Subscription.is_trial == True)
elif filter_type == "imported":
base_query = base_query.where(Subscription.is_imported == True)
count_query = select(func.count()).select_from(base_query.subquery())
total_count_result = await session.execute(count_query)
total_count = total_count_result.scalar() or 0
data_query = base_query.order_by(desc(UserSubscription.created_at)).offset(offset).limit(limit)
result = await session.execute(data_query)
subscriptions_data = []
for row in result.fetchall():
user_sub = row[0]
if user_sub.expires_at <= current_time:
status = "expired"
elif not user_sub.is_active:
status = "inactive"
else:
days_left = (user_sub.expires_at - current_time).days
if days_left <= 3:
status = "expiring_soon"
elif days_left <= 7:
status = "expiring"
else:
status = "active"
subscriptions_data.append({
'id': user_sub.id,
'user_id': row.user_telegram_id,
'user_username': row.user_username or 'N/A',
'user_first_name': row.user_first_name or 'N/A',
'subscription_name': row.subscription_name,
'subscription_price': row.subscription_price,
'short_uuid': user_sub.short_uuid,
'expires_at': user_sub.expires_at,
'is_active': user_sub.is_active,
'auto_pay_enabled': user_sub.auto_pay_enabled,
'auto_pay_days_before': user_sub.auto_pay_days_before,
'created_at': user_sub.created_at,
'is_trial': row.is_trial,
'is_imported': row.is_imported,
'status': status,
'days_left': (user_sub.expires_at - current_time).days if user_sub.expires_at > current_time else 0
})
return subscriptions_data, total_count
except Exception as e:
logger.error(f"Error getting admin user subscriptions: {e}")
return [], 0
async def get_user_subscription_detail_admin(self, subscription_id: int) -> Optional[Dict]:
async with self.session_factory() as session:
try:
from sqlalchemy import select
query = select(
UserSubscription,
Subscription.name.label('subscription_name'),
Subscription.description.label('subscription_description'),
Subscription.price.label('subscription_price'),
Subscription.duration_days.label('subscription_duration'),
Subscription.traffic_limit_gb.label('subscription_traffic_limit'),
Subscription.is_trial.label('is_trial'),
Subscription.is_imported.label('is_imported'),
User.username.label('user_username'),
User.first_name.label('user_first_name'),
User.telegram_id.label('user_telegram_id'),
User.balance.label('user_balance')
).select_from(
UserSubscription.__table__.join(
Subscription.__table__, UserSubscription.subscription_id == Subscription.id
).join(
User.__table__, UserSubscription.user_id == User.telegram_id
)
).where(UserSubscription.id == subscription_id)
result = await session.execute(query)
row = result.fetchone()
if not row:
return None
user_sub = row[0]
current_time = datetime.utcnow()
if user_sub.expires_at <= current_time:
status = "expired"
status_emoji = ""
elif not user_sub.is_active:
status = "inactive"
status_emoji = ""
else:
days_left = (user_sub.expires_at - current_time).days
if days_left <= 1:
status = "expiring_today"
status_emoji = "🔴"
elif days_left <= 3:
status = "expiring_soon"
status_emoji = "🟡"
elif days_left <= 7:
status = "expiring"
status_emoji = "🟠"
else:
status = "active"
status_emoji = "🟢"
return {
'id': user_sub.id,
'user_id': row.user_telegram_id,
'user_username': row.user_username or 'N/A',
'user_first_name': row.user_first_name or 'N/A',
'user_balance': row.user_balance,
'subscription_name': row.subscription_name,
'subscription_description': row.subscription_description,
'subscription_price': row.subscription_price,
'subscription_duration': row.subscription_duration,
'subscription_traffic_limit': row.subscription_traffic_limit,
'short_uuid': user_sub.short_uuid,
'expires_at': user_sub.expires_at,
'is_active': user_sub.is_active,
'auto_pay_enabled': user_sub.auto_pay_enabled,
'auto_pay_days_before': user_sub.auto_pay_days_before,
'created_at': user_sub.created_at,
'updated_at': user_sub.updated_at,
'is_trial': row.is_trial,
'is_imported': row.is_imported,
'status': status,
'status_emoji': status_emoji,
'days_left': (user_sub.expires_at - current_time).days if user_sub.expires_at > current_time else 0
}
except Exception as e:
logger.error(f"Error getting user subscription detail: {e}")
return None
async def get_user_subscriptions_stats_admin(self) -> Dict[str, Any]:
async with self.session_factory() as session:
try:
from sqlalchemy import select, func, and_, or_
from datetime import datetime, timedelta
current_time = datetime.utcnow()
total_subs = await session.execute(
select(func.count(UserSubscription.id))
)
total_subscriptions = total_subs.scalar() or 0
active_subs = await session.execute(
select(func.count(UserSubscription.id)).where(
and_(
UserSubscription.is_active == True,
UserSubscription.expires_at > current_time
)
)
)
active_subscriptions = active_subs.scalar() or 0
expired_subs = await session.execute(
select(func.count(UserSubscription.id)).where(
or_(
UserSubscription.is_active == False,
UserSubscription.expires_at <= current_time
)
)
)
expired_subscriptions = expired_subs.scalar() or 0
autopay_subs = await session.execute(
select(func.count(UserSubscription.id)).where(
UserSubscription.auto_pay_enabled == True
)
)
autopay_subscriptions = autopay_subs.scalar() or 0
expiring_date = current_time + timedelta(days=7)
expiring_subs = await session.execute(
select(func.count(UserSubscription.id)).where(
and_(
UserSubscription.is_active == True,
UserSubscription.expires_at > current_time,
UserSubscription.expires_at <= expiring_date
)
)
)
expiring_subscriptions = expiring_subs.scalar() or 0
trial_subs = await session.execute(
select(func.count(UserSubscription.id))
.select_from(
UserSubscription.__table__.join(
Subscription.__table__, UserSubscription.subscription_id == Subscription.id
)
)
.where(Subscription.is_trial == True)
)
trial_subscriptions = trial_subs.scalar() or 0
imported_subs = await session.execute(
select(func.count(UserSubscription.id))
.select_from(
UserSubscription.__table__.join(
Subscription.__table__, UserSubscription.subscription_id == Subscription.id
)
)
.where(Subscription.is_imported == True)
)
imported_subscriptions = imported_subs.scalar() or 0
return {
'total_subscriptions': total_subscriptions,
'active_subscriptions': active_subscriptions,
'expired_subscriptions': expired_subscriptions,
'autopay_subscriptions': autopay_subscriptions,
'expiring_subscriptions': expiring_subscriptions,
'trial_subscriptions': trial_subscriptions,
'imported_subscriptions': imported_subscriptions
}
except Exception as e:
logger.error(f"Error getting user subscriptions stats: {e}")
return {
'total_subscriptions': 0,
'active_subscriptions': 0,
'expired_subscriptions': 0,
'autopay_subscriptions': 0,
'expiring_subscriptions': 0,
'trial_subscriptions': 0,
'imported_subscriptions': 0
}
async def get_lucky_game_admin_stats(self) -> dict:
async with self.session_factory() as session:
try:
from sqlalchemy import select, func, case, and_
from datetime import date
total_games_result = await session.execute(
select(func.count(LuckyGame.id))
)
total_games = total_games_result.scalar() or 0
total_wins_result = await session.execute(
select(func.count(LuckyGame.id)).where(LuckyGame.is_winner == True)
)
total_wins = total_wins_result.scalar() or 0
unique_players_result = await session.execute(
select(func.count(func.distinct(LuckyGame.user_id)))
)
unique_players = unique_players_result.scalar() or 0
total_rewards_result = await session.execute(
select(func.sum(LuckyGame.reward_amount))
)
total_rewards = total_rewards_result.scalar() or 0.0
avg_reward_result = await session.execute(
select(func.avg(LuckyGame.reward_amount)).where(LuckyGame.is_winner == True)
)
avg_reward = avg_reward_result.scalar() or 0.0
today = date.today()
games_today_result = await session.execute(
select(func.count(LuckyGame.id)).where(
func.date(LuckyGame.played_at) == today
)
)
games_today = games_today_result.scalar() or 0
wins_today_result = await session.execute(
select(func.count(LuckyGame.id)).where(
and_(
func.date(LuckyGame.played_at) == today,
LuckyGame.is_winner == True
)
)
)
wins_today = wins_today_result.scalar() or 0
last_game_result = await session.execute(
select(func.max(LuckyGame.played_at))
)
last_game = last_game_result.scalar()
first_game_result = await session.execute(
select(func.min(LuckyGame.played_at))
)
first_game = first_game_result.scalar()
stats = {
'total_games': total_games,
'total_wins': total_wins,
'unique_players': unique_players,
'total_rewards': float(total_rewards),
'avg_reward': float(avg_reward),
'games_today': games_today,
'wins_today': wins_today,
'last_game': last_game.isoformat() if last_game else None,
'first_game': first_game.isoformat() if first_game else None
}
if stats['total_games'] > 0:
stats['win_rate'] = (stats['total_wins'] / stats['total_games']) * 100
stats['win_rate_today'] = (stats['wins_today'] / stats['games_today']) * 100 if stats['games_today'] > 0 else 0
else:
stats['win_rate'] = 0
stats['win_rate_today'] = 0
return stats
except Exception as e:
logger.error(f"Error getting lucky game admin stats: {e}")
return {
'total_games': 0, 'total_wins': 0, 'unique_players': 0,
'total_rewards': 0.0, 'avg_reward': 0.0, 'games_today': 0,
'wins_today': 0, 'win_rate': 0, 'win_rate_today': 0,
'last_game': None, 'first_game': None
}
async def get_lucky_game_top_players(self, limit: int = 5) -> List[dict]:
"""Получает топ игроков по выигрышам"""
async with self.session_factory() as session:
try:
from sqlalchemy import select, func, desc
# Запрос с группировкой по пользователям
query = select(
LuckyGame.user_id,
User.username,
User.first_name,
func.count(LuckyGame.id).label('games_played'),
func.count(case((LuckyGame.is_winner == True, 1))).label('wins'),
func.sum(LuckyGame.reward_amount).label('total_won'),
func.max(LuckyGame.played_at).label('last_game')
).select_from(
LuckyGame.__table__.join(User.__table__, LuckyGame.user_id == User.telegram_id, isouter=True)
).group_by(
LuckyGame.user_id, User.username, User.first_name
).order_by(
desc('total_won'), desc('wins')
).limit(limit)
result = await session.execute(query)
rows = result.all()
return [
{
'user_id': row.user_id,
'username': row.username or 'N/A',
'first_name': row.first_name or 'Unknown',
'games_played': row.games_played,
'wins': row.wins,
'total_won': float(row.total_won or 0),
'last_game': row.last_game.isoformat() if row.last_game else None,
'win_rate': (row.wins / row.games_played) * 100 if row.games_played > 0 else 0
}
for row in rows
]
except Exception as e:
logger.error(f"Error getting lucky game top players: {e}")
return []