chore: run lint and prettifier

This commit is contained in:
3252a8
2026-05-12 21:54:12 +03:00
parent f31540afdb
commit 11187487b4
174 changed files with 12383 additions and 6688 deletions
+12 -12
View File
@@ -1,13 +1,15 @@
from . import user_dal
from . import payment_dal
from . import subscription_dal
from . import promo_code_dal
from . import panel_sync_dal
from . import message_log_dal
from . import user_billing_dal
from . import ad_dal
from . import security_dal
from . import app_settings_dal
from . import (
ad_dal,
app_settings_dal,
message_log_dal,
panel_sync_dal,
payment_dal,
promo_code_dal,
security_dal,
subscription_dal,
user_billing_dal,
user_dal,
)
__all__ = (
"user_dal",
@@ -21,5 +23,3 @@ __all__ = (
"security_dal",
"app_settings_dal",
)
+18 -14
View File
@@ -1,11 +1,11 @@
import logging
from typing import Optional, List, Dict, Any, Tuple
from typing import Any, Dict, List, Optional
from sqlalchemy import and_, func, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from sqlalchemy.orm import selectinload
from sqlalchemy import update, delete, func, and_
from ..models import AdCampaign, AdAttribution, Payment
from ..models import AdAttribution, AdCampaign, Payment
async def create_campaign(
@@ -31,7 +31,9 @@ async def get_campaign_by_id(session: AsyncSession, campaign_id: int) -> Optiona
return result.scalar_one_or_none()
async def get_campaign_by_start_param(session: AsyncSession, start_param: str) -> Optional[AdCampaign]:
async def get_campaign_by_start_param(
session: AsyncSession, start_param: str
) -> Optional[AdCampaign]:
clean = start_param.strip()
stmt = select(AdCampaign).where(AdCampaign.start_param == clean)
result = await session.execute(stmt)
@@ -56,7 +58,9 @@ async def toggle_campaign_active(session: AsyncSession, campaign_id: int, is_act
return result.rowcount > 0
async def ensure_attribution(session: AsyncSession, *, user_id: int, campaign_id: int) -> AdAttribution:
async def ensure_attribution(
session: AsyncSession, *, user_id: int, campaign_id: int
) -> AdAttribution:
existing = await get_attribution_for_user(session, user_id)
if existing:
return existing
@@ -93,7 +97,10 @@ async def get_campaign_stats(session: AsyncSession, campaign_id: int) -> Dict[st
# Trials
trials_stmt = select(func.count(AdAttribution.user_id)).where(
and_(AdAttribution.ad_campaign_id == campaign_id, AdAttribution.trial_activated_at.is_not(None))
and_(
AdAttribution.ad_campaign_id == campaign_id,
AdAttribution.trial_activated_at.is_not(None),
)
)
trials = (await session.execute(trials_stmt)).scalar() or 0
@@ -165,13 +172,10 @@ async def get_totals(session: AsyncSession) -> Dict[str, float]:
total_cost = float((await session.execute(total_cost_stmt)).scalar() or 0.0)
# Total revenue from all attributed users (unique users counted across all campaigns)
attrib_subq = (
select(
AdAttribution.user_id.label("user_id"),
AdAttribution.first_start_at.label("first_start_at"),
)
.subquery()
)
attrib_subq = select(
AdAttribution.user_id.label("user_id"),
AdAttribution.first_start_at.label("first_start_at"),
).subquery()
revenue_stmt = (
select(func.coalesce(func.sum(Payment.amount), 0.0))
.select_from(Payment)
+36 -24
View File
@@ -1,14 +1,14 @@
import logging
from typing import Optional, List
from typing import List, Optional
from sqlalchemy import func, or_
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from sqlalchemy import func, or_
from ..models import MessageLog, User
from ..models import MessageLog
async def create_message_log(session: AsyncSession,
log_data: dict) -> Optional[MessageLog]:
async def create_message_log(session: AsyncSession, log_data: dict) -> Optional[MessageLog]:
try:
log_entry = await create_message_log_no_commit(session, log_data)
@@ -17,15 +17,12 @@ async def create_message_log(session: AsyncSession,
return log_entry
except Exception as e:
await session.rollback()
logging.error(f"Failed to create and commit message log: {e}",
exc_info=True)
logging.error(f"Failed to create and commit message log: {e}", exc_info=True)
return None
async def get_all_message_logs(session: AsyncSession, limit: int,
offset: int) -> List[MessageLog]:
stmt = select(MessageLog).order_by(
MessageLog.timestamp.desc()).limit(limit).offset(offset)
async def get_all_message_logs(session: AsyncSession, limit: int, offset: int) -> List[MessageLog]:
stmt = select(MessageLog).order_by(MessageLog.timestamp.desc()).limit(limit).offset(offset)
result = await session.execute(stmt)
return result.scalars().all()
@@ -36,30 +33,45 @@ async def count_all_message_logs(session: AsyncSession) -> int:
return result.scalar_one()
async def get_user_message_logs(session: AsyncSession, user_id_to_search: int,
limit: int, offset: int) -> List[MessageLog]:
stmt = (select(MessageLog).where(
or_(MessageLog.user_id == user_id_to_search,
MessageLog.target_user_id == user_id_to_search)).order_by(
MessageLog.timestamp.desc()).limit(limit).offset(offset))
async def get_user_message_logs(
session: AsyncSession, user_id_to_search: int, limit: int, offset: int
) -> List[MessageLog]:
stmt = (
select(MessageLog)
.where(
or_(
MessageLog.user_id == user_id_to_search,
MessageLog.target_user_id == user_id_to_search,
)
)
.order_by(MessageLog.timestamp.desc())
.limit(limit)
.offset(offset)
)
result = await session.execute(stmt)
return result.scalars().all()
async def count_user_message_logs(session: AsyncSession,
user_id_to_search: int) -> int:
stmt = (select(func.count()).select_from(MessageLog).where(
or_(MessageLog.user_id == user_id_to_search,
MessageLog.target_user_id == user_id_to_search)))
async def count_user_message_logs(session: AsyncSession, user_id_to_search: int) -> int:
stmt = (
select(func.count())
.select_from(MessageLog)
.where(
or_(
MessageLog.user_id == user_id_to_search,
MessageLog.target_user_id == user_id_to_search,
)
)
)
result = await session.execute(stmt)
return result.scalar_one()
async def create_message_log_no_commit(session: AsyncSession,
log_data: dict) -> MessageLog:
async def create_message_log_no_commit(session: AsyncSession, log_data: dict) -> MessageLog:
if log_data.get("target_user_id"):
from .user_dal import get_user_by_id
target_user = await get_user_by_id(session, log_data["target_user_id"])
if not target_user:
logging.warning(
+13 -13
View File
@@ -1,27 +1,26 @@
import logging
from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from sqlalchemy import update
from datetime import datetime, timezone
from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession
from db.models import PanelSyncStatus
SINGLETON_ID = 1
async def get_panel_sync_status(
session: AsyncSession) -> Optional[PanelSyncStatus]:
async def get_panel_sync_status(session: AsyncSession) -> Optional[PanelSyncStatus]:
return await session.get(PanelSyncStatus, SINGLETON_ID)
async def update_panel_sync_status(
session: AsyncSession,
status: str,
details: str,
users_processed: int = 0,
subs_synced: int = 0,
last_sync_time: Optional[datetime] = None) -> PanelSyncStatus:
session: AsyncSession,
status: str,
details: str,
users_processed: int = 0,
subs_synced: int = 0,
last_sync_time: Optional[datetime] = None,
) -> PanelSyncStatus:
if last_sync_time is None:
last_sync_time = datetime.now(timezone.utc)
@@ -39,7 +38,8 @@ async def update_panel_sync_status(
status=status,
details=details,
users_processed_from_panel=users_processed,
subscriptions_synced=subs_synced)
subscriptions_synced=subs_synced,
)
session.add(sync_record)
await session.flush()
+75 -97
View File
@@ -1,62 +1,57 @@
import logging
from typing import Optional, List, Dict, Any
from typing import Any, Dict, List, Optional
from sqlalchemy import Date, and_, cast, func
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from sqlalchemy import update, func, and_, cast, Date
from sqlalchemy.orm import selectinload
from db.models import Payment, User
async def create_payment_record(session: AsyncSession,
payment_data: Dict[str, Any]) -> Payment:
async def create_payment_record(session: AsyncSession, payment_data: Dict[str, Any]) -> Payment:
from .user_dal import get_user_by_id
user = await get_user_by_id(session, payment_data["user_id"])
if not user:
raise ValueError(
f"User with id {payment_data['user_id']} not found for creating payment."
)
raise ValueError(f"User with id {payment_data['user_id']} not found for creating payment.")
if payment_data.get("promo_code_id"):
from .promo_code_dal import get_promo_code_by_id
promo = await get_promo_code_by_id(session,
payment_data["promo_code_id"])
promo = await get_promo_code_by_id(session, payment_data["promo_code_id"])
if not promo:
raise ValueError(
f"Promo code with id {payment_data['promo_code_id']} not found."
)
raise ValueError(f"Promo code with id {payment_data['promo_code_id']} not found.")
new_payment = Payment(**payment_data)
session.add(new_payment)
await session.flush()
await session.refresh(new_payment)
logging.info(
f"Payment record {new_payment.payment_id} created for user {new_payment.user_id}"
)
logging.info(f"Payment record {new_payment.payment_id} created for user {new_payment.user_id}")
return new_payment
async def get_payment_by_provider_payment_id(
session: AsyncSession, provider_payment_id: str) -> Optional[Payment]:
session: AsyncSession, provider_payment_id: str
) -> Optional[Payment]:
"""Fetch a payment by provider-specific identifier."""
stmt = select(Payment).where(
Payment.provider_payment_id == provider_payment_id)
stmt = select(Payment).where(Payment.provider_payment_id == provider_payment_id)
result = await session.execute(stmt)
return result.scalar_one_or_none()
async def ensure_payment_with_provider_id(
session: AsyncSession,
*,
user_id: int,
amount: float,
currency: str,
months: int,
description: str,
provider: str,
provider_payment_id: str) -> Payment:
session: AsyncSession,
*,
user_id: int,
amount: float,
currency: str,
months: int,
description: str,
provider: str,
provider_payment_id: str,
) -> Payment:
"""Idempotently create a payment record for a provider event.
If a payment with the same provider_payment_id already exists, returns it.
@@ -80,20 +75,20 @@ async def ensure_payment_with_provider_id(
return await create_payment_record(session, payment_payload)
async def get_payment_by_db_id(session: AsyncSession,
payment_db_id: int) -> Optional[Payment]:
async def get_payment_by_db_id(session: AsyncSession, payment_db_id: int) -> Optional[Payment]:
stmt = select(Payment).where(Payment.payment_id == payment_db_id).options(
selectinload(Payment.user), selectinload(Payment.promo_code_used))
stmt = (
select(Payment)
.where(Payment.payment_id == payment_db_id)
.options(selectinload(Payment.user), selectinload(Payment.promo_code_used))
)
result = await session.execute(stmt)
return result.scalar_one_or_none()
async def update_payment_status_by_db_id(
session: AsyncSession,
payment_db_id: int,
new_status: str,
yk_payment_id: Optional[str] = None) -> Optional[Payment]:
session: AsyncSession, payment_db_id: int, new_status: str, yk_payment_id: Optional[str] = None
) -> Optional[Payment]:
payment = await get_payment_by_db_id(session, payment_db_id)
if payment:
payment.status = new_status
@@ -102,39 +97,42 @@ async def update_payment_status_by_db_id(
payment.yookassa_payment_id = yk_payment_id
await session.flush()
await session.refresh(payment)
logging.info(
f"Payment record {payment.payment_id} status updated to {new_status}."
)
logging.info(f"Payment record {payment.payment_id} status updated to {new_status}.")
else:
logging.warning(
f"Payment record with DB ID {payment_db_id} not found for status update."
)
logging.warning(f"Payment record with DB ID {payment_db_id} not found for status update.")
return payment
async def get_recent_payment_logs_with_user(session: AsyncSession,
limit: int = 20,
offset: int = 0) -> List[Payment]:
stmt = (select(Payment).options(selectinload(Payment.user))
.where(Payment.status == 'succeeded')
.order_by(Payment.created_at.desc())
.limit(limit).offset(offset))
async def get_recent_payment_logs_with_user(
session: AsyncSession, limit: int = 20, offset: int = 0
) -> List[Payment]:
stmt = (
select(Payment)
.options(selectinload(Payment.user))
.where(Payment.status == "succeeded")
.order_by(Payment.created_at.desc())
.limit(limit)
.offset(offset)
)
result = await session.execute(stmt)
return result.scalars().all()
async def get_payments_count(session: AsyncSession) -> int:
"""Get total count of successful payments."""
stmt = select(func.count(Payment.payment_id)).where(Payment.status == 'succeeded')
stmt = select(func.count(Payment.payment_id)).where(Payment.status == "succeeded")
result = await session.execute(stmt)
return result.scalar() or 0
async def get_all_succeeded_payments_with_user(session: AsyncSession) -> List[Payment]:
"""Get all successful payments with user data for export."""
stmt = (select(Payment).options(selectinload(Payment.user))
.where(Payment.status == 'succeeded')
.order_by(Payment.created_at.desc()))
stmt = (
select(Payment)
.options(selectinload(Payment.user))
.where(Payment.status == "succeeded")
.order_by(Payment.created_at.desc())
)
result = await session.execute(stmt)
return result.scalars().all()
@@ -148,7 +146,7 @@ async def count_user_succeeded_payments(
from the count. Useful to check "prior" payments while processing the
current payment in the same transaction.
"""
conditions = [Payment.user_id == user_id, Payment.status == 'succeeded']
conditions = [Payment.user_id == user_id, Payment.status == "succeeded"]
if exclude_payment_id is not None:
conditions.append(Payment.payment_id != exclude_payment_id)
stmt = select(func.count(Payment.payment_id)).where(and_(*conditions))
@@ -157,8 +155,8 @@ async def count_user_succeeded_payments(
async def update_provider_payment_and_status(
session: AsyncSession, payment_db_id: int,
provider_payment_id: str, new_status: str) -> Optional[Payment]:
session: AsyncSession, payment_db_id: int, provider_payment_id: str, new_status: str
) -> Optional[Payment]:
payment = await get_payment_by_db_id(session, payment_db_id)
if payment:
payment.status = new_status
@@ -170,9 +168,7 @@ async def update_provider_payment_and_status(
f"Payment record {payment.payment_id} updated with provider id {provider_payment_id} and status {new_status}."
)
else:
logging.warning(
f"Payment record with DB ID {payment_db_id} not found for provider update."
)
logging.warning(f"Payment record with DB ID {payment_db_id} not found for provider update.")
return payment
@@ -214,54 +210,43 @@ async def _daily_revenue_series_utc(session: AsyncSession, days: int = 14) -> Li
async def get_financial_statistics(session: AsyncSession) -> Dict[str, Any]:
"""Get comprehensive financial statistics."""
from datetime import datetime, timedelta
from sqlalchemy import and_, text
from sqlalchemy import and_
now = datetime.utcnow()
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
week_start = today_start - timedelta(days=7)
month_start = today_start - timedelta(days=30)
# Today's revenue
stmt_today = select(func.sum(Payment.amount)).where(
and_(
Payment.status == 'succeeded',
Payment.created_at >= today_start
)
and_(Payment.status == "succeeded", Payment.created_at >= today_start)
)
today_revenue = await session.execute(stmt_today)
today_amount = today_revenue.scalar() or 0
# Week revenue
stmt_week = select(func.sum(Payment.amount)).where(
and_(
Payment.status == 'succeeded',
Payment.created_at >= week_start
)
and_(Payment.status == "succeeded", Payment.created_at >= week_start)
)
week_revenue = await session.execute(stmt_week)
week_amount = week_revenue.scalar() or 0
# Month revenue
stmt_month = select(func.sum(Payment.amount)).where(
and_(
Payment.status == 'succeeded',
Payment.created_at >= month_start
)
and_(Payment.status == "succeeded", Payment.created_at >= month_start)
)
month_revenue = await session.execute(stmt_month)
month_amount = month_revenue.scalar() or 0
# All time revenue
stmt_all = select(func.sum(Payment.amount)).where(Payment.status == 'succeeded')
stmt_all = select(func.sum(Payment.amount)).where(Payment.status == "succeeded")
all_revenue = await session.execute(stmt_all)
all_amount = all_revenue.scalar() or 0
# Count of successful payments today
stmt_count_today = select(func.count(Payment.payment_id)).where(
and_(
Payment.status == 'succeeded',
Payment.created_at >= today_start
)
and_(Payment.status == "succeeded", Payment.created_at >= today_start)
)
today_count = await session.execute(stmt_count_today)
today_payments_count = today_count.scalar() or 0
@@ -281,10 +266,7 @@ async def get_financial_statistics(session: AsyncSession) -> Dict[str, Any]:
async def get_user_total_paid(session: AsyncSession, user_id: int) -> float:
"""Get total amount paid by a specific user (sum of all succeeded payments)."""
stmt = select(func.sum(Payment.amount)).where(
and_(
Payment.user_id == user_id,
Payment.status == 'succeeded'
)
and_(Payment.user_id == user_id, Payment.status == "succeeded")
)
result = await session.execute(stmt)
total = result.scalar()
@@ -293,19 +275,15 @@ async def get_user_total_paid(session: AsyncSession, user_id: int) -> float:
async def get_referral_revenue(session: AsyncSession, referrer_id: int) -> float:
"""Get total revenue generated from referred users' payments.
This calculates the sum of all succeeded payments made by users
where referred_by_id equals the referrer_id.
"""
from db.models import User
stmt = select(func.sum(Payment.amount)).join(
User, Payment.user_id == User.user_id
).where(
and_(
User.referred_by_id == referrer_id,
Payment.status == 'succeeded'
)
stmt = (
select(func.sum(Payment.amount))
.join(User, Payment.user_id == User.user_id)
.where(and_(User.referred_by_id == referrer_id, Payment.status == "succeeded"))
)
result = await session.execute(stmt)
total = result.scalar()
+69 -52
View File
@@ -1,27 +1,24 @@
import logging
from typing import Optional, List, Dict, Any
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional
from sqlalchemy import func, or_
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from sqlalchemy import update, func, and_, or_
from datetime import datetime, timezone
from db.models import PromoCode, PromoCodeActivation, User, Payment
from db.models import PromoCode, PromoCodeActivation
async def create_promo_code(session: AsyncSession,
promo_data: Dict[str, Any]) -> PromoCode:
async def create_promo_code(session: AsyncSession, promo_data: Dict[str, Any]) -> PromoCode:
new_promo = PromoCode(**promo_data)
session.add(new_promo)
await session.flush()
await session.refresh(new_promo)
logging.info(
f"Promo code '{new_promo.code}' created with ID {new_promo.promo_code_id}"
)
logging.info(f"Promo code '{new_promo.code}' created with ID {new_promo.promo_code_id}")
return new_promo
async def get_promo_code_by_id(session: AsyncSession,
promo_code_id: int) -> Optional[PromoCode]:
async def get_promo_code_by_id(session: AsyncSession, promo_code_id: int) -> Optional[PromoCode]:
return await session.get(PromoCode, promo_code_id)
@@ -33,33 +30,40 @@ async def get_promo_code_by_code(session: AsyncSession, code_str: str) -> Option
async def get_active_promo_code_by_code_str(
session: AsyncSession, code_str: str) -> Optional[PromoCode]:
session: AsyncSession, code_str: str
) -> Optional[PromoCode]:
stmt = select(PromoCode).where(
PromoCode.code == code_str.upper(), PromoCode.is_active == True,
PromoCode.code == code_str.upper(),
PromoCode.is_active == True,
PromoCode.current_activations < PromoCode.max_activations,
or_(PromoCode.valid_until == None, PromoCode.valid_until
> datetime.now(timezone.utc)))
or_(PromoCode.valid_until == None, PromoCode.valid_until > datetime.now(timezone.utc)),
)
result = await session.execute(stmt)
return result.scalar_one_or_none()
async def get_all_active_promo_codes(session: AsyncSession,
limit: int = 20,
offset: int = 0) -> List[PromoCode]:
stmt = (select(PromoCode).where(
PromoCode.is_active == True,
or_(PromoCode.valid_until == None, PromoCode.valid_until
> datetime.now(timezone.utc))).order_by(
PromoCode.created_at.desc()).limit(limit).offset(offset))
async def get_all_active_promo_codes(
session: AsyncSession, limit: int = 20, offset: int = 0
) -> List[PromoCode]:
stmt = (
select(PromoCode)
.where(
PromoCode.is_active == True,
or_(PromoCode.valid_until == None, PromoCode.valid_until > datetime.now(timezone.utc)),
)
.order_by(PromoCode.created_at.desc())
.limit(limit)
.offset(offset)
)
result = await session.execute(stmt)
return result.scalars().all()
async def get_all_promo_codes_with_details(session: AsyncSession, limit: int = 50,
offset: int = 0) -> List[PromoCode]:
async def get_all_promo_codes_with_details(
session: AsyncSession, limit: int = 50, offset: int = 0
) -> List[PromoCode]:
"""Get all promo codes (active and inactive) with pagination for management"""
stmt = (select(PromoCode).order_by(
PromoCode.created_at.desc()).limit(limit).offset(offset))
stmt = select(PromoCode).order_by(PromoCode.created_at.desc()).limit(limit).offset(offset)
result = await session.execute(stmt)
return result.scalars().all()
@@ -67,17 +71,22 @@ async def get_all_promo_codes_with_details(session: AsyncSession, limit: int = 5
async def get_promo_codes_count(session: AsyncSession) -> int:
"""Get total count of all promo codes"""
from sqlalchemy import func
stmt = select(func.count(PromoCode.promo_code_id))
result = await session.execute(stmt)
return result.scalar_one()
async def get_promo_activations_by_code_id(session: AsyncSession, promo_code_id: int, limit: Optional[int] = None, offset: int = 0) -> List[PromoCodeActivation]:
async def get_promo_activations_by_code_id(
session: AsyncSession, promo_code_id: int, limit: Optional[int] = None, offset: int = 0
) -> List[PromoCodeActivation]:
"""Get activation history for a specific promo code with optional pagination."""
stmt = (select(PromoCodeActivation)
.where(PromoCodeActivation.promo_code_id == promo_code_id)
.order_by(PromoCodeActivation.activated_at.desc())
.offset(offset))
stmt = (
select(PromoCodeActivation)
.where(PromoCodeActivation.promo_code_id == promo_code_id)
.order_by(PromoCodeActivation.activated_at.desc())
.offset(offset)
)
if limit is not None:
stmt = stmt.limit(limit)
result = await session.execute(stmt)
@@ -86,13 +95,18 @@ async def get_promo_activations_by_code_id(session: AsyncSession, promo_code_id:
async def count_promo_activations_by_code_id(session: AsyncSession, promo_code_id: int) -> int:
"""Count total activations for a specific promo code."""
stmt = select(func.count()).select_from(PromoCodeActivation).where(PromoCodeActivation.promo_code_id == promo_code_id)
stmt = (
select(func.count())
.select_from(PromoCodeActivation)
.where(PromoCodeActivation.promo_code_id == promo_code_id)
)
result = await session.execute(stmt)
return result.scalar_one()
async def update_promo_code(session: AsyncSession, promo_id: int,
update_data: Dict[str, Any]) -> Optional[PromoCode]:
async def update_promo_code(
session: AsyncSession, promo_id: int, update_data: Dict[str, Any]
) -> Optional[PromoCode]:
promo = await get_promo_code_by_id(session, promo_id)
if not promo:
return None
@@ -111,14 +125,15 @@ async def delete_promo_code(session: AsyncSession, promo_id: int) -> Optional[Pr
activations = await get_promo_activations_by_code_id(session, promo_id)
for activation in activations:
await session.delete(activation)
await session.delete(promo)
await session.flush()
return promo
async def increment_promo_code_usage(
session: AsyncSession, promo_code_id: int) -> Optional[PromoCode]:
session: AsyncSession, promo_code_id: int
) -> Optional[PromoCode]:
promo = await get_promo_code_by_id(session, promo_code_id)
if promo:
if promo.current_activations < promo.max_activations:
@@ -135,22 +150,24 @@ async def increment_promo_code_usage(
async def get_user_activation_for_promo(
session: AsyncSession, promo_code_id: int,
user_id: int) -> Optional[PromoCodeActivation]:
stmt = select(PromoCodeActivation).where(
PromoCodeActivation.promo_code_id == promo_code_id,
PromoCodeActivation.user_id == user_id).limit(1)
session: AsyncSession, promo_code_id: int, user_id: int
) -> Optional[PromoCodeActivation]:
stmt = (
select(PromoCodeActivation)
.where(
PromoCodeActivation.promo_code_id == promo_code_id,
PromoCodeActivation.user_id == user_id,
)
.limit(1)
)
result = await session.execute(stmt)
return result.scalar_one_or_none()
async def record_promo_activation(
session: AsyncSession,
promo_code_id: int,
user_id: int,
payment_id: Optional[int] = None) -> Optional[PromoCodeActivation]:
existing_activation = await get_user_activation_for_promo(
session, promo_code_id, user_id)
session: AsyncSession, promo_code_id: int, user_id: int, payment_id: Optional[int] = None
) -> Optional[PromoCodeActivation]:
existing_activation = await get_user_activation_for_promo(session, promo_code_id, user_id)
if existing_activation:
logging.info(
f"User {user_id} has already activated promo code {promo_code_id}. Activation ID: {existing_activation.activation_id}"
@@ -158,6 +175,7 @@ async def record_promo_activation(
return existing_activation
from .user_dal import get_user_by_id
user = await get_user_by_id(session, user_id)
promo = await get_promo_code_by_id(session, promo_code_id)
if not user or not promo:
@@ -168,18 +186,17 @@ async def record_promo_activation(
if payment_id:
from .payment_dal import get_payment_by_db_id
payment = await get_payment_by_db_id(session, payment_id)
if not payment:
logging.error(
f"Cannot record promo activation: Payment {payment_id} not found."
)
logging.error(f"Cannot record promo activation: Payment {payment_id} not found.")
return None
activation_data = {
"promo_code_id": promo_code_id,
"user_id": user_id,
"payment_id": payment_id,
"activated_at": datetime.now(timezone.utc)
"activated_at": datetime.now(timezone.utc),
}
new_activation = PromoCodeActivation(**activation_data)
session.add(new_activation)
+91 -81
View File
@@ -1,18 +1,18 @@
import logging
from typing import Optional, List, Dict, Any
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional
from sqlalchemy import delete, func, or_, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from sqlalchemy import update, delete, func, and_, or_
from sqlalchemy.orm import selectinload
from datetime import datetime, timezone, timedelta
from db.models import Subscription, User
from db.models import Subscription
async def get_active_subscription_by_user_id(
session: AsyncSession,
user_id: int,
panel_user_uuid: Optional[str] = None) -> Optional[Subscription]:
session: AsyncSession, user_id: int, panel_user_uuid: Optional[str] = None
) -> Optional[Subscription]:
stmt = select(Subscription).where(
Subscription.user_id == user_id,
Subscription.is_active == True,
@@ -26,26 +26,29 @@ async def get_active_subscription_by_user_id(
async def get_subscription_by_panel_subscription_uuid(
session: AsyncSession, panel_sub_uuid: str) -> Optional[Subscription]:
stmt = select(Subscription).where(
Subscription.panel_subscription_uuid == panel_sub_uuid)
session: AsyncSession, panel_sub_uuid: str
) -> Optional[Subscription]:
stmt = select(Subscription).where(Subscription.panel_subscription_uuid == panel_sub_uuid)
result = await session.execute(stmt)
return result.scalar_one_or_none()
async def get_active_subscriptions_for_user(session: AsyncSession, user_id: int) -> List[Subscription]:
async def get_active_subscriptions_for_user(
session: AsyncSession, user_id: int
) -> List[Subscription]:
"""Get all active subscriptions for a user."""
stmt = select(Subscription).where(
Subscription.user_id == user_id,
Subscription.is_active == True
).order_by(Subscription.end_date.desc())
stmt = (
select(Subscription)
.where(Subscription.user_id == user_id, Subscription.is_active == True)
.order_by(Subscription.end_date.desc())
)
result = await session.execute(stmt)
return result.scalars().all()
async def update_subscription(
session: AsyncSession, subscription_id: int,
update_data: Dict[str, Any]) -> Optional[Subscription]:
session: AsyncSession, subscription_id: int, update_data: Dict[str, Any]
) -> Optional[Subscription]:
sub = await session.get(Subscription, subscription_id)
if sub:
for key, value in update_data.items():
@@ -55,20 +58,24 @@ async def update_subscription(
return sub
async def set_auto_renew(session: AsyncSession, subscription_id: int, enabled: bool) -> Optional[Subscription]:
async def set_auto_renew(
session: AsyncSession, subscription_id: int, enabled: bool
) -> Optional[Subscription]:
"""Toggle auto_renew_enabled for a subscription."""
return await update_subscription(session, subscription_id, {"auto_renew_enabled": enabled})
async def set_user_subscriptions_cancelled_with_grace(
session: AsyncSession, user_id: int, grace_days: int = 1) -> int:
session: AsyncSession, user_id: int, grace_days: int = 1
) -> int:
"""Mark all active user subscriptions as cancelled with a short grace period.
Sets end_date to now + grace_days, status_from_panel to 'CANCELLED', and
skip future notifications to reduce noise after cancellation.
Returns number of updated rows.
"""
from datetime import datetime, timezone, timedelta
from datetime import datetime, timedelta, timezone
grace_end = datetime.now(timezone.utc) + timedelta(days=grace_days)
stmt = (
update(Subscription)
@@ -83,14 +90,12 @@ async def set_user_subscriptions_cancelled_with_grace(
return result.rowcount or 0
async def upsert_subscription(session: AsyncSession,
sub_payload: Dict[str, Any]) -> Subscription:
async def upsert_subscription(session: AsyncSession, sub_payload: Dict[str, Any]) -> Subscription:
panel_sub_uuid = sub_payload.get("panel_subscription_uuid")
if not panel_sub_uuid:
raise ValueError("panel_subscription_uuid is required for upsert.")
existing_sub = await get_subscription_by_panel_subscription_uuid(
session, panel_sub_uuid)
existing_sub = await get_subscription_by_panel_subscription_uuid(session, panel_sub_uuid)
if existing_sub:
logging.info(
@@ -103,18 +108,15 @@ async def upsert_subscription(session: AsyncSession,
await session.refresh(existing_sub)
return existing_sub
else:
logging.info(
f"Creating new subscription with panel_sub_uuid {panel_sub_uuid}")
logging.info(f"Creating new subscription with panel_sub_uuid {panel_sub_uuid}")
if sub_payload.get(
"user_id") is None and "panel_user_uuid" not in sub_payload:
raise ValueError(
"For a new subscription without user_id, panel_user_uuid is required."
)
if sub_payload.get("user_id") is None and "panel_user_uuid" not in sub_payload:
raise ValueError("For a new subscription without user_id, panel_user_uuid is required.")
if "end_date" not in sub_payload:
raise ValueError("Missing 'end_date' for new subscription.")
if sub_payload.get("user_id") is not None:
from .user_dal import get_user_by_id
user = await get_user_by_id(session, sub_payload["user_id"])
if not user:
raise ValueError(
@@ -129,15 +131,18 @@ async def upsert_subscription(session: AsyncSession,
async def deactivate_other_active_subscriptions(
session: AsyncSession, panel_user_uuid: str,
current_panel_subscription_uuid: Optional[str]) -> None:
stmt = (update(Subscription).where(
Subscription.panel_user_uuid == panel_user_uuid,
Subscription.is_active == True,
).values(is_active=False, status_from_panel="INACTIVE_BY_BOT_SYNC"))
session: AsyncSession, panel_user_uuid: str, current_panel_subscription_uuid: Optional[str]
) -> None:
stmt = (
update(Subscription)
.where(
Subscription.panel_user_uuid == panel_user_uuid,
Subscription.is_active == True,
)
.values(is_active=False, status_from_panel="INACTIVE_BY_BOT_SYNC")
)
if current_panel_subscription_uuid:
stmt = stmt.where(Subscription.panel_subscription_uuid !=
current_panel_subscription_uuid)
stmt = stmt.where(Subscription.panel_subscription_uuid != current_panel_subscription_uuid)
result = await session.execute(stmt)
if result.rowcount > 0:
@@ -146,8 +151,7 @@ async def deactivate_other_active_subscriptions(
)
async def deactivate_all_user_subscriptions(
session: AsyncSession, user_id: int) -> int:
async def deactivate_all_user_subscriptions(session: AsyncSession, user_id: int) -> int:
stmt = (
update(Subscription)
.where(Subscription.user_id == user_id, Subscription.is_active == True)
@@ -161,8 +165,7 @@ async def deactivate_all_user_subscriptions(
return result.rowcount
async def delete_all_user_subscriptions(
session: AsyncSession, user_id: int) -> int:
async def delete_all_user_subscriptions(session: AsyncSession, user_id: int) -> int:
"""Completely delete all user subscriptions (for trial reset)"""
stmt = delete(Subscription).where(Subscription.user_id == user_id)
result = await session.execute(stmt)
@@ -174,70 +177,77 @@ async def delete_all_user_subscriptions(
async def update_subscription_end_date(
session: AsyncSession, subscription_id: int,
new_end_date: datetime) -> Optional[Subscription]:
session: AsyncSession, subscription_id: int, new_end_date: datetime
) -> Optional[Subscription]:
return await update_subscription(
session, subscription_id, {
session,
subscription_id,
{
"end_date": new_end_date,
"last_notification_sent": None,
"is_active": True,
"status_from_panel": "ACTIVE_EXTENDED_BY_BOT"
})
"status_from_panel": "ACTIVE_EXTENDED_BY_BOT",
},
)
async def has_any_subscription_for_user(session: AsyncSession,
user_id: int) -> bool:
stmt = select(Subscription.subscription_id).where(
Subscription.user_id == user_id).limit(1)
async def has_any_subscription_for_user(session: AsyncSession, user_id: int) -> bool:
stmt = select(Subscription.subscription_id).where(Subscription.user_id == user_id).limit(1)
result = await session.execute(stmt)
return result.scalar_one_or_none() is not None
async def get_subscriptions_near_expiration(
session: AsyncSession, days_threshold: int) -> List[Subscription]:
session: AsyncSession, days_threshold: int
) -> List[Subscription]:
now_utc = datetime.now(timezone.utc)
threshold_date = now_utc + timedelta(days=days_threshold)
stmt = (select(Subscription).join(Subscription.user).where(
Subscription.is_active == True,
Subscription.skip_notifications == False,
Subscription.end_date > now_utc,
Subscription.end_date <= threshold_date,
or_(
Subscription.last_notification_sent == None,
func.date(Subscription.last_notification_sent)
< func.date(now_utc))).order_by(
Subscription.end_date.asc()).options(
selectinload(Subscription.user)))
stmt = (
select(Subscription)
.join(Subscription.user)
.where(
Subscription.is_active == True,
Subscription.skip_notifications == False,
Subscription.end_date > now_utc,
Subscription.end_date <= threshold_date,
or_(
Subscription.last_notification_sent == None,
func.date(Subscription.last_notification_sent) < func.date(now_utc),
),
)
.order_by(Subscription.end_date.asc())
.options(selectinload(Subscription.user))
)
result = await session.execute(stmt)
return result.scalars().all()
async def update_subscription_notification_time(
session: AsyncSession, subscription_id: int,
notification_time: datetime) -> Optional[Subscription]:
session: AsyncSession, subscription_id: int, notification_time: datetime
) -> Optional[Subscription]:
return await update_subscription(
session, subscription_id,
{"last_notification_sent": notification_time})
session, subscription_id, {"last_notification_sent": notification_time}
)
async def find_subscription_for_notification_update(
session: AsyncSession, user_id: int,
subscription_end_date_to_match: datetime) -> Optional[Subscription]:
session: AsyncSession, user_id: int, subscription_end_date_to_match: datetime
) -> Optional[Subscription]:
if subscription_end_date_to_match.tzinfo is None:
subscription_end_date_to_match = subscription_end_date_to_match.replace(
tzinfo=timezone.utc)
subscription_end_date_to_match = subscription_end_date_to_match.replace(tzinfo=timezone.utc)
stmt = select(Subscription).where(
Subscription.user_id == user_id, Subscription.is_active == True,
Subscription.end_date
>= subscription_end_date_to_match - timedelta(seconds=1),
Subscription.end_date
<= subscription_end_date_to_match + timedelta(seconds=1)).limit(1)
stmt = (
select(Subscription)
.where(
Subscription.user_id == user_id,
Subscription.is_active == True,
Subscription.end_date >= subscription_end_date_to_match - timedelta(seconds=1),
Subscription.end_date <= subscription_end_date_to_match + timedelta(seconds=1),
)
.limit(1)
)
result = await session.execute(stmt)
return result.scalar_one_or_none()
+28 -9
View File
@@ -1,6 +1,7 @@
from typing import Optional, Dict, Any, List
from sqlalchemy.ext.asyncio import AsyncSession
from typing import List, Optional
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.sql import func
from db.models import UserBilling, UserPaymentMethod
@@ -65,7 +66,9 @@ async def upsert_user_payment_method(
card_network: Optional[str] = None,
set_default: bool = False,
) -> UserPaymentMethod:
existing_stmt = select(UserPaymentMethod).where(UserPaymentMethod.provider_payment_method_id == provider_payment_method_id)
existing_stmt = select(UserPaymentMethod).where(
UserPaymentMethod.provider_payment_method_id == provider_payment_method_id
)
result = await session.execute(existing_stmt)
existing: Optional[UserPaymentMethod] = result.scalar_one_or_none()
if existing:
@@ -103,7 +106,9 @@ async def upsert_user_payment_method(
return record
async def list_user_payment_methods(session: AsyncSession, user_id: int, provider: Optional[str] = None) -> List[UserPaymentMethod]:
async def list_user_payment_methods(
session: AsyncSession, user_id: int, provider: Optional[str] = None
) -> List[UserPaymentMethod]:
stmt = select(UserPaymentMethod).where(UserPaymentMethod.user_id == user_id)
if provider:
stmt = stmt.where(UserPaymentMethod.provider == provider)
@@ -112,7 +117,9 @@ async def list_user_payment_methods(session: AsyncSession, user_id: int, provide
return result.scalars().all()
async def get_user_default_payment_method(session: AsyncSession, user_id: int, provider: str = "yookassa") -> Optional[UserPaymentMethod]:
async def get_user_default_payment_method(
session: AsyncSession, user_id: int, provider: str = "yookassa"
) -> Optional[UserPaymentMethod]:
stmt = select(UserPaymentMethod).where(
UserPaymentMethod.user_id == user_id,
UserPaymentMethod.provider == provider,
@@ -122,17 +129,29 @@ async def get_user_default_payment_method(session: AsyncSession, user_id: int, p
return result.scalar_one_or_none()
async def set_user_default_payment_method(session: AsyncSession, user_id: int, method_id: int) -> bool:
async def set_user_default_payment_method(
session: AsyncSession, user_id: int, method_id: int
) -> bool:
methods = await list_user_payment_methods(session, user_id)
if not any(m.method_id == method_id for m in methods):
return False
await session.execute(update(UserPaymentMethod).where(UserPaymentMethod.user_id == user_id).values(is_default=False))
await session.execute(update(UserPaymentMethod).where(UserPaymentMethod.method_id == method_id).values(is_default=True))
await session.execute(
update(UserPaymentMethod)
.where(UserPaymentMethod.user_id == user_id)
.values(is_default=False)
)
await session.execute(
update(UserPaymentMethod)
.where(UserPaymentMethod.method_id == method_id)
.values(is_default=True)
)
return True
async def delete_user_payment_method(session: AsyncSession, user_id: int, method_id: int) -> bool:
stmt = select(UserPaymentMethod).where(UserPaymentMethod.method_id == method_id, UserPaymentMethod.user_id == user_id)
stmt = select(UserPaymentMethod).where(
UserPaymentMethod.method_id == method_id, UserPaymentMethod.user_id == user_id
)
result = await session.execute(stmt)
method = result.scalar_one_or_none()
if not method:
+44 -61
View File
@@ -1,25 +1,25 @@
import logging
import secrets
import string
from typing import Optional, List, Dict, Any, Tuple
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import and_, delete, desc, func, or_, update
from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from sqlalchemy.orm import selectinload
from sqlalchemy import update, delete, func, and_, or_, desc
from sqlalchemy.orm import aliased
from datetime import datetime, timezone, timedelta
from sqlalchemy.dialects.postgresql import insert as pg_insert
from ..models import (
User,
UserTelegramAvatar,
Subscription,
AdAttribution,
MessageLog,
Payment,
PromoCodeActivation,
MessageLog,
Subscription,
User,
UserBilling,
UserPaymentMethod,
AdAttribution,
UserTelegramAvatar,
)
REFERRAL_CODE_ALPHABET = string.ascii_uppercase + string.digits
@@ -33,9 +33,7 @@ class UserMergeConflictError(ValueError):
def _generate_referral_code_candidate() -> str:
return "".join(
secrets.choice(REFERRAL_CODE_ALPHABET) for _ in range(REFERRAL_CODE_LENGTH)
)
return "".join(secrets.choice(REFERRAL_CODE_ALPHABET) for _ in range(REFERRAL_CODE_LENGTH))
async def _referral_code_exists(session: AsyncSession, code: str) -> bool:
@@ -105,9 +103,7 @@ async def get_user_by_email(session: AsyncSession, email: str) -> Optional[User]
return result.scalar_one_or_none()
async def get_user_by_telegram_id(
session: AsyncSession, telegram_id: int
) -> Optional[User]:
async def get_user_by_telegram_id(session: AsyncSession, telegram_id: int) -> Optional[User]:
stmt = select(User).where(User.telegram_id == telegram_id)
result = await session.execute(stmt)
return result.scalar_one_or_none()
@@ -152,9 +148,7 @@ async def upsert_user_telegram_avatar(
return avatar
async def get_user_by_panel_uuid(
session: AsyncSession, panel_uuid: str
) -> Optional[User]:
async def get_user_by_panel_uuid(session: AsyncSession, panel_uuid: str) -> Optional[User]:
stmt = select(User).where(User.panel_user_uuid == panel_uuid)
result = await session.execute(stmt)
return result.scalar_one_or_none()
@@ -198,9 +192,7 @@ async def create_user(session: AsyncSession, user_data: Dict[str, Any]) -> Tuple
f"New user {user.user_id} created in DAL. Referred by: {user.referred_by_id or 'N/A'}."
)
elif user is not None:
logging.info(
f"User {user.user_id} already exists in DAL. Proceeding without creation."
)
logging.info(f"User {user.user_id} already exists in DAL. Proceeding without creation.")
return user, created
@@ -399,7 +391,10 @@ async def merge_users(
for attr in ("username", "first_name", "last_name", "language_code", "telegram_photo_url"):
if not getattr(target, attr) and getattr(source, attr):
setattr(target, attr, getattr(source, attr))
if not target.channel_subscription_verified and source.channel_subscription_verified is not None:
if (
not target.channel_subscription_verified
and source.channel_subscription_verified is not None
):
target.channel_subscription_verified = source.channel_subscription_verified
if not target.channel_subscription_checked_at and source.channel_subscription_checked_at:
target.channel_subscription_checked_at = source.channel_subscription_checked_at
@@ -407,8 +402,8 @@ async def merge_users(
target.channel_subscription_verified_for = source.channel_subscription_verified_for
if source.lifetime_used_traffic_bytes is not None:
target.lifetime_used_traffic_bytes = (
(target.lifetime_used_traffic_bytes or 0) + source.lifetime_used_traffic_bytes
)
target.lifetime_used_traffic_bytes or 0
) + source.lifetime_used_traffic_bytes
if not target.referred_by_id and source.referred_by_id != target_user_id:
target.referred_by_id = source.referred_by_id
if target.referred_by_id == source_user_id:
@@ -456,9 +451,7 @@ async def merge_users(
)
).scalar_one_or_none()
if target_has_attribution:
await session.execute(
delete(AdAttribution).where(AdAttribution.user_id == source_user_id)
)
await session.execute(delete(AdAttribution).where(AdAttribution.user_id == source_user_id))
else:
await session.execute(
update(AdAttribution)
@@ -492,9 +485,7 @@ async def merge_users(
)
for model in (Payment, PromoCodeActivation, UserPaymentMethod):
await session.execute(
update(model)
.where(model.user_id == source_user_id)
.values(user_id=target_user_id)
update(model).where(model.user_id == source_user_id).values(user_id=target_user_id)
)
await session.execute(
@@ -540,9 +531,7 @@ async def update_user(
return user
async def update_user_language(
session: AsyncSession, user_id: int, lang_code: str
) -> bool:
async def update_user_language(session: AsyncSession, user_id: int, lang_code: str) -> bool:
stmt = update(User).where(User.user_id == user_id).values(language_code=lang_code)
result = await session.execute(stmt)
return result.rowcount > 0
@@ -550,11 +539,7 @@ async def update_user_language(
async def get_banned_users(session: AsyncSession) -> List[User]:
"""Get all banned users"""
stmt = (
select(User)
.where(User.is_banned == True)
.order_by(User.registration_date.desc())
)
stmt = select(User).where(User.is_banned == True).order_by(User.registration_date.desc())
result = await session.execute(stmt)
return result.scalars().all()
@@ -597,23 +582,25 @@ async def get_all_users_with_panel_uuid(session: AsyncSession) -> List[User]:
async def get_enhanced_user_statistics(session: AsyncSession) -> Dict[str, Any]:
"""Get comprehensive user statistics including active users, trial users, etc."""
from datetime import datetime, timezone
# Use timezone-aware UTC to avoid naive/aware comparison issues in SQL queries
now = datetime.now(timezone.utc)
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
# Total users
total_users_stmt = select(func.count(User.user_id))
total_users = (await session.execute(total_users_stmt)).scalar() or 0
# Banned users
banned_users_stmt = select(func.count(User.user_id)).where(User.is_banned == True)
banned_users = (await session.execute(banned_users_stmt)).scalar() or 0
# Active users today (proxy: registered today)
active_today_stmt = select(func.count(User.user_id)).where(User.registration_date >= today_start)
active_today_stmt = select(func.count(User.user_id)).where(
User.registration_date >= today_start
)
active_today = (await session.execute(active_today_stmt)).scalar() or 0
# Users with active paid subscriptions (non-trial providers only)
paid_subs_stmt = (
select(func.count(func.distinct(Subscription.user_id)))
@@ -622,12 +609,12 @@ async def get_enhanced_user_statistics(session: AsyncSession) -> Dict[str, Any]:
and_(
Subscription.is_active == True,
Subscription.end_date > now,
Subscription.provider.is_not(None) # Not trial
Subscription.provider.is_not(None), # Not trial
)
)
)
paid_subs_users = (await session.execute(paid_subs_stmt)).scalar() or 0
# Users on trial period
trial_subs_stmt = (
select(func.count(func.distinct(Subscription.user_id)))
@@ -636,19 +623,19 @@ async def get_enhanced_user_statistics(session: AsyncSession) -> Dict[str, Any]:
and_(
Subscription.is_active == True,
Subscription.end_date > now,
Subscription.provider.is_(None) # Trial subscriptions
Subscription.provider.is_(None), # Trial subscriptions
)
)
)
trial_users = (await session.execute(trial_subs_stmt)).scalar() or 0
# Inactive users (no active subscription)
inactive_users = total_users - paid_subs_users - trial_users - banned_users
# Users attracted via referral
referral_users_stmt = select(func.count(User.user_id)).where(User.referred_by_id.is_not(None))
referral_users = (await session.execute(referral_users_stmt)).scalar() or 0
return {
"total_users": total_users,
"banned_users": banned_users,
@@ -656,13 +643,14 @@ async def get_enhanced_user_statistics(session: AsyncSession) -> Dict[str, Any]:
"paid_subscriptions": paid_subs_users,
"trial_users": trial_users,
"inactive_users": max(0, inactive_users),
"referral_users": referral_users
"referral_users": referral_users,
}
async def get_user_ids_with_active_subscription(session: AsyncSession) -> List[int]:
"""Return non-banned user IDs who have an active subscription (paid or trial)."""
from datetime import datetime, timezone
now = datetime.now(timezone.utc)
stmt = (
@@ -683,6 +671,7 @@ async def get_user_ids_with_active_subscription(session: AsyncSession) -> List[i
async def get_user_ids_without_active_subscription(session: AsyncSession) -> List[int]:
"""Return non-banned user IDs who do NOT have any active subscription."""
from datetime import datetime, timezone
now = datetime.now(timezone.utc)
active_subs = aliased(Subscription)
@@ -729,15 +718,9 @@ async def delete_user_and_relations(session: AsyncSession, user_id: int) -> bool
)
)
await session.execute(delete(Payment).where(Payment.user_id == user_id))
await session.execute(
delete(Subscription).where(Subscription.user_id == user_id)
)
await session.execute(
delete(PromoCodeActivation).where(PromoCodeActivation.user_id == user_id)
)
await session.execute(
delete(UserPaymentMethod).where(UserPaymentMethod.user_id == user_id)
)
await session.execute(delete(Subscription).where(Subscription.user_id == user_id))
await session.execute(delete(PromoCodeActivation).where(PromoCodeActivation.user_id == user_id))
await session.execute(delete(UserPaymentMethod).where(UserPaymentMethod.user_id == user_id))
await session.execute(delete(UserBilling).where(UserBilling.user_id == user_id))
await session.execute(delete(AdAttribution).where(AdAttribution.user_id == user_id))
await session.execute(delete(UserTelegramAvatar).where(UserTelegramAvatar.user_id == user_id))
+24 -29
View File
@@ -1,9 +1,11 @@
import logging
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import sessionmaker
from config.settings import Settings
from db.models import Base
from .migrator import run_database_migrations
async_engine = None
@@ -13,9 +15,7 @@ def init_db_connection(settings: Settings) -> sessionmaker:
global async_engine
if async_engine is None:
logging.info(
f"Attempting to create SQLAlchemy engine with URL: {settings.DATABASE_URL}"
)
logging.info(f"Attempting to create SQLAlchemy engine with URL: {settings.DATABASE_URL}")
async_engine = create_async_engine(
settings.DATABASE_URL,
echo=False,
@@ -31,17 +31,14 @@ def init_db_connection(settings: Settings) -> sessionmaker:
autocommit=False,
autoflush=False,
)
logging.info(
f"SQLAlchemy Async Engine and SessionFactory configured for PostgreSQL."
)
logging.info("SQLAlchemy Async Engine and SessionFactory configured for PostgreSQL.")
return local_async_session_factory
async def get_async_session(session_factory: sessionmaker) -> AsyncSession:
if session_factory is None:
raise RuntimeError(
"AsyncSessionFactory is not provided or initialized.")
raise RuntimeError("AsyncSessionFactory is not provided or initialized.")
async_session = session_factory()
try:
@@ -54,10 +51,7 @@ async def init_db(settings: Settings, session_factory: sessionmaker):
global async_engine
if async_engine is None:
logging.warning(
"init_db: async_engine was None, re-initializing via init_db_connection."
)
logging.warning("init_db: async_engine was None, re-initializing via init_db_connection.")
raise RuntimeError(
"async_engine is not initialized. Call init_db_connection and get session_factory first."
@@ -66,41 +60,42 @@ async def init_db(settings: Settings, session_factory: sessionmaker):
async with async_engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
await conn.run_sync(run_database_migrations)
logging.info(
"PostgreSQL database initialized/checked successfully using SQLAlchemy."
)
logging.info("PostgreSQL database initialized/checked successfully using SQLAlchemy.")
try:
from bot.services.settings_override_service import load_overrides_from_db
await load_overrides_from_db(settings, session_factory)
except Exception as e_overrides:
logging.warning(
f"Failed to load setting overrides on startup: {e_overrides}"
)
logging.warning(f"Failed to load setting overrides on startup: {e_overrides}")
async with session_factory() as session:
from .dal.panel_sync_dal import get_panel_sync_status, update_panel_sync_status
from sqlalchemy import text
from .dal.panel_sync_dal import get_panel_sync_status, update_panel_sync_status
try:
current_status = await get_panel_sync_status(session)
if current_status is None:
logging.info("Initializing panel_sync_status record.")
await update_panel_sync_status(session,
status="never_run",
details="System initialized",
users_processed=0,
subs_synced=0)
await update_panel_sync_status(
session,
status="never_run",
details="System initialized",
users_processed=0,
subs_synced=0,
)
await session.commit()
except Exception as e_sync_init:
await session.rollback()
logging.error(
f"Failed to initialize PanelSyncStatus: {e_sync_init}",
exc_info=True)
logging.error(f"Failed to initialize PanelSyncStatus: {e_sync_init}", exc_info=True)
if settings.tariffs_config:
try:
default_tariff = settings.tariffs_config.default
default_price = default_tariff.period_price(1, "rub") or default_tariff.min_period_price_rub()
default_price = (
default_tariff.period_price(1, "rub") or default_tariff.min_period_price_rub()
)
await session.execute(
text(
"""
+94 -61
View File
@@ -32,17 +32,13 @@ def _migration_0001_add_channel_subscription_fields(connection: Connection) -> N
statements: List[str] = []
if "channel_subscription_verified" not in columns:
statements.append(
"ALTER TABLE users ADD COLUMN channel_subscription_verified BOOLEAN"
)
statements.append("ALTER TABLE users ADD COLUMN channel_subscription_verified BOOLEAN")
if "channel_subscription_checked_at" not in columns:
statements.append(
"ALTER TABLE users ADD COLUMN channel_subscription_checked_at TIMESTAMPTZ"
)
if "channel_subscription_verified_for" not in columns:
statements.append(
"ALTER TABLE users ADD COLUMN channel_subscription_verified_for BIGINT"
)
statements.append("ALTER TABLE users ADD COLUMN channel_subscription_verified_for BIGINT")
for stmt in statements:
connection.execute(text(stmt))
@@ -53,9 +49,7 @@ def _migration_0002_add_referral_code(connection: Connection) -> None:
columns: Set[str] = {col["name"] for col in inspector.get_columns("users")}
if "referral_code" not in columns:
connection.execute(
text("ALTER TABLE users ADD COLUMN referral_code VARCHAR(16)")
)
connection.execute(text("ALTER TABLE users ADD COLUMN referral_code VARCHAR(16)"))
connection.execute(
text(
@@ -119,11 +113,7 @@ def _migration_0004_add_lifetime_used_traffic(connection: Connection) -> None:
if "lifetime_used_traffic_bytes" in columns:
return
connection.execute(
text(
"ALTER TABLE users ADD COLUMN lifetime_used_traffic_bytes BIGINT"
)
)
connection.execute(text("ALTER TABLE users ADD COLUMN lifetime_used_traffic_bytes BIGINT"))
def _migration_0005_add_email_auth_fields(connection: Connection) -> None:
@@ -133,9 +123,7 @@ def _migration_0005_add_email_auth_fields(connection: Connection) -> None:
if "email" not in columns:
connection.execute(text("ALTER TABLE users ADD COLUMN email VARCHAR"))
if "email_verified_at" not in columns:
connection.execute(
text("ALTER TABLE users ADD COLUMN email_verified_at TIMESTAMPTZ")
)
connection.execute(text("ALTER TABLE users ADD COLUMN email_verified_at TIMESTAMPTZ"))
if "telegram_id" not in columns:
connection.execute(text("ALTER TABLE users ADD COLUMN telegram_id BIGINT"))
@@ -246,9 +234,7 @@ def _migration_0007_add_telegram_photo_url(connection: Connection) -> None:
if "telegram_photo_url" in columns:
return
connection.execute(
text("ALTER TABLE users ADD COLUMN telegram_photo_url TEXT")
)
connection.execute(text("ALTER TABLE users ADD COLUMN telegram_photo_url TEXT"))
def _migration_0008_add_email_verification_code_status(connection: Connection) -> None:
@@ -288,9 +274,7 @@ def _migration_0010_add_email_magic_token_hash(connection: Connection) -> None:
if "magic_token_hash" not in columns:
connection.execute(
text(
"ALTER TABLE email_verification_codes ADD COLUMN magic_token_hash VARCHAR"
)
text("ALTER TABLE email_verification_codes ADD COLUMN magic_token_hash VARCHAR")
)
connection.execute(
@@ -338,29 +322,49 @@ def _migration_0012_add_tariffs_schema(connection: Connection) -> None:
if "tier_baseline_bytes" not in sub_columns:
sub_statements.append("ALTER TABLE subscriptions ADD COLUMN tier_baseline_bytes BIGINT")
if "topup_balance_bytes" not in sub_columns:
sub_statements.append("ALTER TABLE subscriptions ADD COLUMN topup_balance_bytes BIGINT NOT NULL DEFAULT 0")
sub_statements.append(
"ALTER TABLE subscriptions ADD COLUMN topup_balance_bytes BIGINT NOT NULL DEFAULT 0"
)
if "premium_baseline_bytes" not in sub_columns:
sub_statements.append("ALTER TABLE subscriptions ADD COLUMN premium_baseline_bytes BIGINT NOT NULL DEFAULT 0")
sub_statements.append(
"ALTER TABLE subscriptions ADD COLUMN premium_baseline_bytes BIGINT NOT NULL DEFAULT 0"
)
if "premium_topup_balance_bytes" not in sub_columns:
sub_statements.append("ALTER TABLE subscriptions ADD COLUMN premium_topup_balance_bytes BIGINT NOT NULL DEFAULT 0")
sub_statements.append(
"ALTER TABLE subscriptions ADD COLUMN premium_topup_balance_bytes BIGINT NOT NULL DEFAULT 0"
)
if "premium_topup_used_bytes" not in sub_columns:
sub_statements.append("ALTER TABLE subscriptions ADD COLUMN premium_topup_used_bytes BIGINT NOT NULL DEFAULT 0")
sub_statements.append(
"ALTER TABLE subscriptions ADD COLUMN premium_topup_used_bytes BIGINT NOT NULL DEFAULT 0"
)
if "premium_used_bytes" not in sub_columns:
sub_statements.append("ALTER TABLE subscriptions ADD COLUMN premium_used_bytes BIGINT NOT NULL DEFAULT 0")
sub_statements.append(
"ALTER TABLE subscriptions ADD COLUMN premium_used_bytes BIGINT NOT NULL DEFAULT 0"
)
if "premium_is_limited" not in sub_columns:
sub_statements.append("ALTER TABLE subscriptions ADD COLUMN premium_is_limited BOOLEAN NOT NULL DEFAULT FALSE")
sub_statements.append(
"ALTER TABLE subscriptions ADD COLUMN premium_is_limited BOOLEAN NOT NULL DEFAULT FALSE"
)
if "premium_period_start_at" not in sub_columns:
sub_statements.append("ALTER TABLE subscriptions ADD COLUMN premium_period_start_at TIMESTAMPTZ")
sub_statements.append(
"ALTER TABLE subscriptions ADD COLUMN premium_period_start_at TIMESTAMPTZ"
)
if "period_start_at" not in sub_columns:
sub_statements.append("ALTER TABLE subscriptions ADD COLUMN period_start_at TIMESTAMPTZ")
if "is_throttled" not in sub_columns:
sub_statements.append("ALTER TABLE subscriptions ADD COLUMN is_throttled BOOLEAN NOT NULL DEFAULT FALSE")
sub_statements.append(
"ALTER TABLE subscriptions ADD COLUMN is_throttled BOOLEAN NOT NULL DEFAULT FALSE"
)
if "effective_monthly_price_rub" not in sub_columns:
sub_statements.append("ALTER TABLE subscriptions ADD COLUMN effective_monthly_price_rub NUMERIC")
sub_statements.append(
"ALTER TABLE subscriptions ADD COLUMN effective_monthly_price_rub NUMERIC"
)
if "hwid_device_limit" not in sub_columns:
sub_statements.append("ALTER TABLE subscriptions ADD COLUMN hwid_device_limit INTEGER")
if "extra_hwid_devices" not in sub_columns:
sub_statements.append("ALTER TABLE subscriptions ADD COLUMN extra_hwid_devices INTEGER NOT NULL DEFAULT 0")
sub_statements.append(
"ALTER TABLE subscriptions ADD COLUMN extra_hwid_devices INTEGER NOT NULL DEFAULT 0"
)
for stmt in sub_statements:
connection.execute(text(stmt))
@@ -488,17 +492,29 @@ def _migration_0014_add_premium_squad_traffic_fields(connection: Connection) ->
sub_columns: Set[str] = {col["name"] for col in inspector.get_columns("subscriptions")}
statements: List[str] = []
if "premium_baseline_bytes" not in sub_columns:
statements.append("ALTER TABLE subscriptions ADD COLUMN premium_baseline_bytes BIGINT NOT NULL DEFAULT 0")
statements.append(
"ALTER TABLE subscriptions ADD COLUMN premium_baseline_bytes BIGINT NOT NULL DEFAULT 0"
)
if "premium_topup_balance_bytes" not in sub_columns:
statements.append("ALTER TABLE subscriptions ADD COLUMN premium_topup_balance_bytes BIGINT NOT NULL DEFAULT 0")
statements.append(
"ALTER TABLE subscriptions ADD COLUMN premium_topup_balance_bytes BIGINT NOT NULL DEFAULT 0"
)
if "premium_topup_used_bytes" not in sub_columns:
statements.append("ALTER TABLE subscriptions ADD COLUMN premium_topup_used_bytes BIGINT NOT NULL DEFAULT 0")
statements.append(
"ALTER TABLE subscriptions ADD COLUMN premium_topup_used_bytes BIGINT NOT NULL DEFAULT 0"
)
if "premium_used_bytes" not in sub_columns:
statements.append("ALTER TABLE subscriptions ADD COLUMN premium_used_bytes BIGINT NOT NULL DEFAULT 0")
statements.append(
"ALTER TABLE subscriptions ADD COLUMN premium_used_bytes BIGINT NOT NULL DEFAULT 0"
)
if "premium_is_limited" not in sub_columns:
statements.append("ALTER TABLE subscriptions ADD COLUMN premium_is_limited BOOLEAN NOT NULL DEFAULT FALSE")
statements.append(
"ALTER TABLE subscriptions ADD COLUMN premium_is_limited BOOLEAN NOT NULL DEFAULT FALSE"
)
if "premium_period_start_at" not in sub_columns:
statements.append("ALTER TABLE subscriptions ADD COLUMN premium_period_start_at TIMESTAMPTZ")
statements.append(
"ALTER TABLE subscriptions ADD COLUMN premium_period_start_at TIMESTAMPTZ"
)
for stmt in statements:
connection.execute(text(stmt))
connection.execute(
@@ -513,9 +529,13 @@ def _migration_0015_add_premium_topup_carryover_fields(connection: Connection) -
sub_columns: Set[str] = {col["name"] for col in inspector.get_columns("subscriptions")}
statements: List[str] = []
if "premium_topup_used_bytes" not in sub_columns:
statements.append("ALTER TABLE subscriptions ADD COLUMN premium_topup_used_bytes BIGINT NOT NULL DEFAULT 0")
statements.append(
"ALTER TABLE subscriptions ADD COLUMN premium_topup_used_bytes BIGINT NOT NULL DEFAULT 0"
)
if "premium_period_start_at" not in sub_columns:
statements.append("ALTER TABLE subscriptions ADD COLUMN premium_period_start_at TIMESTAMPTZ")
statements.append(
"ALTER TABLE subscriptions ADD COLUMN premium_period_start_at TIMESTAMPTZ"
)
for stmt in statements:
connection.execute(text(stmt))
@@ -596,7 +616,9 @@ def _migration_0020_add_regular_bonus_bytes(connection: Connection) -> None:
)
def _migration_0019_clear_subscription_months_for_non_subscription_payments(connection: Connection) -> None:
def _migration_0019_clear_subscription_months_for_non_subscription_payments(
connection: Connection,
) -> None:
"""Null out subscription_duration_months for legacy non-subscription payments.
Older builds stored the raw `months` callback value into
@@ -643,19 +665,33 @@ def _migration_0017_reconcile_legacy_admin_api_schema(connection: Connection) ->
if "tier_baseline_bytes" not in sub_columns:
sub_statements.append("ALTER TABLE subscriptions ADD COLUMN tier_baseline_bytes BIGINT")
if "topup_balance_bytes" not in sub_columns:
sub_statements.append("ALTER TABLE subscriptions ADD COLUMN topup_balance_bytes BIGINT NOT NULL DEFAULT 0")
sub_statements.append(
"ALTER TABLE subscriptions ADD COLUMN topup_balance_bytes BIGINT NOT NULL DEFAULT 0"
)
if "premium_baseline_bytes" not in sub_columns:
sub_statements.append("ALTER TABLE subscriptions ADD COLUMN premium_baseline_bytes BIGINT NOT NULL DEFAULT 0")
sub_statements.append(
"ALTER TABLE subscriptions ADD COLUMN premium_baseline_bytes BIGINT NOT NULL DEFAULT 0"
)
if "premium_topup_balance_bytes" not in sub_columns:
sub_statements.append("ALTER TABLE subscriptions ADD COLUMN premium_topup_balance_bytes BIGINT NOT NULL DEFAULT 0")
sub_statements.append(
"ALTER TABLE subscriptions ADD COLUMN premium_topup_balance_bytes BIGINT NOT NULL DEFAULT 0"
)
if "premium_topup_used_bytes" not in sub_columns:
sub_statements.append("ALTER TABLE subscriptions ADD COLUMN premium_topup_used_bytes BIGINT NOT NULL DEFAULT 0")
sub_statements.append(
"ALTER TABLE subscriptions ADD COLUMN premium_topup_used_bytes BIGINT NOT NULL DEFAULT 0"
)
if "premium_used_bytes" not in sub_columns:
sub_statements.append("ALTER TABLE subscriptions ADD COLUMN premium_used_bytes BIGINT NOT NULL DEFAULT 0")
sub_statements.append(
"ALTER TABLE subscriptions ADD COLUMN premium_used_bytes BIGINT NOT NULL DEFAULT 0"
)
if "premium_is_limited" not in sub_columns:
sub_statements.append("ALTER TABLE subscriptions ADD COLUMN premium_is_limited BOOLEAN NOT NULL DEFAULT FALSE")
sub_statements.append(
"ALTER TABLE subscriptions ADD COLUMN premium_is_limited BOOLEAN NOT NULL DEFAULT FALSE"
)
if "is_throttled" not in sub_columns:
sub_statements.append("ALTER TABLE subscriptions ADD COLUMN is_throttled BOOLEAN NOT NULL DEFAULT FALSE")
sub_statements.append(
"ALTER TABLE subscriptions ADD COLUMN is_throttled BOOLEAN NOT NULL DEFAULT FALSE"
)
for stmt in sub_statements:
connection.execute(text(stmt))
@@ -677,9 +713,13 @@ def _migration_0017_reconcile_legacy_admin_api_schema(connection: Connection) ->
msg_columns: Set[str] = {col["name"] for col in inspector.get_columns("message_logs")}
msg_statements: List[str] = []
if "is_admin_event" not in msg_columns:
msg_statements.append("ALTER TABLE message_logs ADD COLUMN is_admin_event BOOLEAN NOT NULL DEFAULT FALSE")
msg_statements.append(
"ALTER TABLE message_logs ADD COLUMN is_admin_event BOOLEAN NOT NULL DEFAULT FALSE"
)
if "target_user_id" not in msg_columns:
msg_statements.append("ALTER TABLE message_logs ADD COLUMN target_user_id BIGINT REFERENCES users(user_id)")
msg_statements.append(
"ALTER TABLE message_logs ADD COLUMN target_user_id BIGINT REFERENCES users(user_id)"
)
for stmt in msg_statements:
connection.execute(text(stmt))
connection.execute(
@@ -816,26 +856,19 @@ def run_database_migrations(connection: Connection) -> None:
_ensure_migrations_table(connection)
applied_revisions: Set[str] = {
row[0]
for row in connection.execute(
text("SELECT id FROM schema_migrations")
)
row[0] for row in connection.execute(text("SELECT id FROM schema_migrations"))
}
for migration in MIGRATIONS:
if migration.id in applied_revisions:
continue
logging.info(
"Migrator: applying %s %s", migration.id, migration.description
)
logging.info("Migrator: applying %s %s", migration.id, migration.description)
try:
with connection.begin_nested():
migration.upgrade(connection)
connection.execute(
text(
"INSERT INTO schema_migrations (id) VALUES (:revision)"
),
text("INSERT INTO schema_migrations (id) VALUES (:revision)"),
{"revision": migration.id},
)
except Exception as exc:
+82 -90
View File
@@ -1,8 +1,21 @@
from sqlalchemy import create_engine, Column, Integer, String, Boolean, DateTime, Float, ForeignKey, UniqueConstraint, Text, BigInteger, Index, Numeric, LargeBinary
from sqlalchemy.orm import relationship, DeclarativeBase
from sqlalchemy import (
BigInteger,
Boolean,
Column,
DateTime,
Float,
ForeignKey,
Index,
Integer,
LargeBinary,
Numeric,
String,
Text,
UniqueConstraint,
)
from sqlalchemy.ext.asyncio import AsyncAttrs
from sqlalchemy.orm import DeclarativeBase, relationship
from sqlalchemy.sql import func
from datetime import datetime
class Base(AsyncAttrs, DeclarativeBase):
@@ -21,39 +34,36 @@ class User(Base):
first_name = Column(String, nullable=True)
last_name = Column(String, nullable=True)
language_code = Column(String, default="ru")
registration_date = Column(DateTime(timezone=True),
server_default=func.now())
registration_date = Column(DateTime(timezone=True), server_default=func.now())
is_banned = Column(Boolean, default=False)
panel_user_uuid = Column(String, nullable=True, unique=True, index=True)
referral_code = Column(String(16), nullable=True, unique=True, index=True)
referred_by_id = Column(BigInteger,
ForeignKey("users.user_id"),
nullable=True)
referred_by_id = Column(BigInteger, ForeignKey("users.user_id"), nullable=True)
lifetime_used_traffic_bytes = Column(BigInteger, nullable=True)
channel_subscription_verified = Column(Boolean, nullable=True)
channel_subscription_checked_at = Column(DateTime(timezone=True),
nullable=True)
channel_subscription_checked_at = Column(DateTime(timezone=True), nullable=True)
channel_subscription_verified_for = Column(BigInteger, nullable=True)
referrer = relationship("User", remote_side=[user_id], backref="referrals")
subscriptions = relationship("Subscription",
back_populates="user",
cascade="all, delete-orphan")
payments = relationship("Payment",
back_populates="user",
cascade="all, delete-orphan")
promo_code_activations = relationship("PromoCodeActivation",
back_populates="user",
cascade="all, delete-orphan")
message_logs_authored = relationship("MessageLog",
foreign_keys="MessageLog.user_id",
back_populates="author_user",
cascade="all, delete-orphan")
subscriptions = relationship(
"Subscription", back_populates="user", cascade="all, delete-orphan"
)
payments = relationship("Payment", back_populates="user", cascade="all, delete-orphan")
promo_code_activations = relationship(
"PromoCodeActivation", back_populates="user", cascade="all, delete-orphan"
)
message_logs_authored = relationship(
"MessageLog",
foreign_keys="MessageLog.user_id",
back_populates="author_user",
cascade="all, delete-orphan",
)
message_logs_targeted = relationship(
"MessageLog",
foreign_keys="MessageLog.target_user_id",
back_populates="target_user",
cascade="all, delete-orphan")
cascade="all, delete-orphan",
)
def __repr__(self):
return f"<User(user_id={self.user_id}, username='{self.username}')>"
@@ -90,15 +100,9 @@ class Subscription(Base):
)
subscription_id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(BigInteger,
ForeignKey("users.user_id"),
nullable=False,
index=True)
user_id = Column(BigInteger, ForeignKey("users.user_id"), nullable=False, index=True)
panel_user_uuid = Column(String, nullable=False, index=True)
panel_subscription_uuid = Column(String,
unique=True,
index=True,
nullable=True)
panel_subscription_uuid = Column(String, unique=True, index=True, nullable=True)
start_date = Column(DateTime(timezone=True), nullable=True)
end_date = Column(DateTime(timezone=True), nullable=False, index=True)
duration_months = Column(Integer, nullable=True)
@@ -178,19 +182,11 @@ class SecurityThrottle(Base):
class Payment(Base):
__tablename__ = "payments"
__table_args__ = (
Index("ix_payments_user_id_status", "user_id", "status"),
)
__table_args__ = (Index("ix_payments_user_id_status", "user_id", "status"),)
payment_id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(BigInteger,
ForeignKey("users.user_id"),
nullable=False,
index=True)
yookassa_payment_id = Column(String,
unique=True,
index=True,
nullable=True)
user_id = Column(BigInteger, ForeignKey("users.user_id"), nullable=False, index=True)
yookassa_payment_id = Column(String, unique=True, index=True, nullable=True)
provider_payment_id = Column(String, unique=True, nullable=True)
provider = Column(String, nullable=False, default="yookassa", index=True)
idempotence_key = Column(String, unique=True, nullable=True)
@@ -203,24 +199,21 @@ class Payment(Base):
tariff_key = Column(String, nullable=True, index=True)
purchased_gb = Column(Float, nullable=True)
purchased_hwid_devices = Column(Integer, nullable=True)
promo_code_id = Column(Integer,
ForeignKey("promo_codes.promo_code_id"),
nullable=True)
promo_code_id = Column(Integer, ForeignKey("promo_codes.promo_code_id"), nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True),
onupdate=func.now(),
nullable=True)
updated_at = Column(DateTime(timezone=True), onupdate=func.now(), nullable=True)
user = relationship("User", back_populates="payments")
promo_code_used = relationship("PromoCode",
back_populates="payments_where_used")
promo_code_used = relationship("PromoCode", back_populates="payments_where_used")
class TrafficTopup(Base):
__tablename__ = "traffic_topups"
topup_id = Column(Integer, primary_key=True, autoincrement=True)
subscription_id = Column(Integer, ForeignKey("subscriptions.subscription_id"), nullable=False, index=True)
subscription_id = Column(
Integer, ForeignKey("subscriptions.subscription_id"), nullable=False, index=True
)
payment_id = Column(Integer, ForeignKey("payments.payment_id"), nullable=True, index=True)
purchased_bytes = Column(BigInteger, nullable=False)
kind = Column(String, nullable=False, index=True)
@@ -234,7 +227,9 @@ class HwidDevicePurchase(Base):
__tablename__ = "hwid_device_purchases"
purchase_id = Column(Integer, primary_key=True, autoincrement=True)
subscription_id = Column(Integer, ForeignKey("subscriptions.subscription_id"), nullable=False, index=True)
subscription_id = Column(
Integer, ForeignKey("subscriptions.subscription_id"), nullable=False, index=True
)
payment_id = Column(Integer, ForeignKey("payments.payment_id"), nullable=True, index=True)
purchased_devices = Column(Integer, nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
@@ -246,11 +241,15 @@ class HwidDevicePurchase(Base):
class TrafficWarning(Base):
__tablename__ = "traffic_warnings"
__table_args__ = (
UniqueConstraint("subscription_id", "period_start_at", "level", name="uq_traffic_warning_period_level"),
UniqueConstraint(
"subscription_id", "period_start_at", "level", name="uq_traffic_warning_period_level"
),
)
warning_id = Column(Integer, primary_key=True, autoincrement=True)
subscription_id = Column(Integer, ForeignKey("subscriptions.subscription_id"), nullable=False, index=True)
subscription_id = Column(
Integer, ForeignKey("subscriptions.subscription_id"), nullable=False, index=True
)
period_start_at = Column(DateTime(timezone=True), nullable=True)
level = Column(Integer, nullable=False)
traffic_limit_bytes = Column(BigInteger, nullable=True)
@@ -263,7 +262,9 @@ class TariffChange(Base):
__tablename__ = "tariff_changes"
change_id = Column(Integer, primary_key=True, autoincrement=True)
subscription_id = Column(Integer, ForeignKey("subscriptions.subscription_id"), nullable=False, index=True)
subscription_id = Column(
Integer, ForeignKey("subscriptions.subscription_id"), nullable=False, index=True
)
from_tariff_key = Column(String, nullable=True)
to_tariff_key = Column(String, nullable=False)
mode = Column(String, nullable=False, index=True)
@@ -292,6 +293,7 @@ class UserBilling(Base):
user = relationship("User")
class UserPaymentMethod(Base):
__tablename__ = "user_payment_methods"
@@ -307,9 +309,10 @@ class UserPaymentMethod(Base):
user = relationship("User")
__table_args__ = (
UniqueConstraint('user_id', 'provider_payment_method_id', name='uq_user_provider_method'),
UniqueConstraint("user_id", "provider_payment_method_id", name="uq_user_provider_method"),
)
class PromoCode(Base):
__tablename__ = "promo_codes"
@@ -323,63 +326,50 @@ class PromoCode(Base):
created_at = Column(DateTime(timezone=True), server_default=func.now())
valid_until = Column(DateTime(timezone=True), nullable=True)
activations = relationship("PromoCodeActivation",
back_populates="promo_code",
cascade="all, delete-orphan")
payments_where_used = relationship("Payment",
back_populates="promo_code_used")
activations = relationship(
"PromoCodeActivation", back_populates="promo_code", cascade="all, delete-orphan"
)
payments_where_used = relationship("Payment", back_populates="promo_code_used")
class PromoCodeActivation(Base):
__tablename__ = "promo_code_activations"
activation_id = Column(Integer, primary_key=True, autoincrement=True)
promo_code_id = Column(Integer,
ForeignKey("promo_codes.promo_code_id"),
nullable=False)
promo_code_id = Column(Integer, ForeignKey("promo_codes.promo_code_id"), nullable=False)
user_id = Column(BigInteger, ForeignKey("users.user_id"), nullable=False)
activated_at = Column(DateTime(timezone=True), server_default=func.now())
payment_id = Column(Integer,
ForeignKey("payments.payment_id"),
nullable=True)
payment_id = Column(Integer, ForeignKey("payments.payment_id"), nullable=True)
promo_code = relationship("PromoCode", back_populates="activations")
user = relationship("User", back_populates="promo_code_activations")
payment = relationship("Payment")
__table_args__ = (UniqueConstraint('promo_code_id',
'user_id',
name='uq_promo_user_activation'), )
__table_args__ = (
UniqueConstraint("promo_code_id", "user_id", name="uq_promo_user_activation"),
)
class MessageLog(Base):
__tablename__ = "message_logs"
log_id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(BigInteger,
ForeignKey("users.user_id"),
nullable=True,
index=True)
user_id = Column(BigInteger, ForeignKey("users.user_id"), nullable=True, index=True)
telegram_username = Column(String, nullable=True)
telegram_first_name = Column(String, nullable=True)
event_type = Column(String, nullable=False, index=True)
content = Column(Text, nullable=True)
raw_update_preview = Column(Text, nullable=True)
timestamp = Column(DateTime(timezone=True),
server_default=func.now(),
index=True)
timestamp = Column(DateTime(timezone=True), server_default=func.now(), index=True)
is_admin_event = Column(Boolean, default=False)
target_user_id = Column(BigInteger,
ForeignKey("users.user_id"),
nullable=True,
index=True)
target_user_id = Column(BigInteger, ForeignKey("users.user_id"), nullable=True, index=True)
author_user = relationship("User",
foreign_keys=[user_id],
back_populates="message_logs_authored")
target_user = relationship("User",
foreign_keys=[target_user_id],
back_populates="message_logs_targeted")
author_user = relationship(
"User", foreign_keys=[user_id], back_populates="message_logs_authored"
)
target_user = relationship(
"User", foreign_keys=[target_user_id], back_populates="message_logs_targeted"
)
class PanelSyncStatus(Base):
@@ -392,7 +382,7 @@ class PanelSyncStatus(Base):
users_processed_from_panel = Column(Integer, default=0)
subscriptions_synced = Column(Integer, default=0)
__table_args__ = (UniqueConstraint('id'), )
__table_args__ = (UniqueConstraint("id"),)
class AdCampaign(Base):
@@ -419,7 +409,9 @@ class AdAttribution(Base):
__tablename__ = "ad_attributions"
user_id = Column(BigInteger, ForeignKey("users.user_id"), primary_key=True, index=True)
ad_campaign_id = Column(Integer, ForeignKey("ad_campaigns.ad_campaign_id"), nullable=False, index=True)
ad_campaign_id = Column(
Integer, ForeignKey("ad_campaigns.ad_campaign_id"), nullable=False, index=True
)
first_start_at = Column(DateTime(timezone=True), server_default=func.now())
trial_activated_at = Column(DateTime(timezone=True), nullable=True)