Migrating to Postgres from sqlite3

This commit is contained in:
machka
2025-05-20 18:41:32 +00:00
parent ac2f83c061
commit a56d804b58
38 changed files with 5017 additions and 4389 deletions
+76
View File
@@ -0,0 +1,76 @@
import logging
from typing import Optional, List
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from sqlalchemy import func, or_
from ..models import MessageLog, User
async def create_message_log(session: AsyncSession,
log_data: dict) -> Optional[MessageLog]:
try:
log_entry = await create_message_log_no_commit(session, log_data)
await session.commit()
await session.refresh(log_entry)
return log_entry
except Exception as e:
await session.rollback()
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)
result = await session.execute(stmt)
return result.scalars().all()
async def count_all_message_logs(session: AsyncSession) -> int:
stmt = select(func.count()).select_from(MessageLog)
result = await session.execute(stmt)
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))
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)))
result = await session.execute(stmt)
return result.scalar_one()
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(
f"Target user {log_data['target_user_id']} not found for message log. Setting to NULL."
)
log_data["target_user_id"] = None
new_log = MessageLog(**log_data)
session.add(new_log)
logging.debug(
f"Message log added to session: user {log_data.get('user_id')}, event {log_data.get('event_type')}"
)
return new_log
+50
View File
@@ -0,0 +1,50 @@
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 db.models import PanelSyncStatus
SINGLETON_ID = 1
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:
if last_sync_time is None:
last_sync_time = datetime.now(timezone.utc)
sync_record = await get_panel_sync_status(session)
if sync_record:
sync_record.last_sync_time = last_sync_time
sync_record.status = status
sync_record.details = details
sync_record.users_processed_from_panel = users_processed
sync_record.subscriptions_synced = subs_synced
else:
sync_record = PanelSyncStatus(
id=SINGLETON_ID,
last_sync_time=last_sync_time,
status=status,
details=details,
users_processed_from_panel=users_processed,
subscriptions_synced=subs_synced)
session.add(sync_record)
await session.flush()
await session.refresh(sync_record)
logging.info(
f"Panel sync status updated: {status}, Users: {users_processed}, Subs: {subs_synced}"
)
return sync_record
+115
View File
@@ -0,0 +1,115 @@
import logging
from typing import Optional, List, Dict, Any
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from sqlalchemy import update, func
from sqlalchemy.orm import selectinload
from db.models import Payment, User
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."
)
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"])
if not promo:
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}"
)
return new_payment
async def get_payment_by_yookassa_id(
session: AsyncSession, yookassa_payment_id: str) -> Optional[Payment]:
stmt = select(Payment).where(
Payment.yookassa_payment_id == yookassa_payment_id)
result = await session.execute(stmt)
return result.scalar_one_or_none()
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))
result = await session.execute(stmt)
return result.scalar_one_or_none()
async def get_payment_by_db_id_with_promo(
session: AsyncSession, payment_db_id: int) -> Optional[Payment]:
stmt = select(Payment).where(Payment.payment_id == payment_db_id).options(
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]:
payment = await get_payment_by_db_id(session, payment_db_id)
if payment:
payment.status = new_status
payment.updated_at = func.now()
if yk_payment_id and payment.yookassa_payment_id is None:
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}."
)
else:
logging.warning(
f"Payment record with DB ID {payment_db_id} not found for status update."
)
return payment
async def update_payment_status_by_yk_id(session: AsyncSession,
yookassa_payment_id: str,
new_status: str) -> Optional[Payment]:
payment = await get_payment_by_yookassa_id(session, yookassa_payment_id)
if payment:
payment.status = new_status
payment.updated_at = func.now()
await session.flush()
await session.refresh(payment)
logging.info(
f"Payment record with YK ID {yookassa_payment_id} status updated to {new_status}."
)
else:
logging.warning(
f"Payment record with YK ID {yookassa_payment_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)).order_by(
Payment.created_at.desc()).limit(limit).offset(offset))
result = await session.execute(stmt)
return result.scalars().all()
+126
View File
@@ -0,0 +1,126 @@
import logging
from typing import Optional, List, Dict, Any
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
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}"
)
return new_promo
async def get_promo_code_by_id(session: AsyncSession,
promo_code_id: int) -> Optional[PromoCode]:
return await session.get(PromoCode, promo_code_id)
async def get_active_promo_code_by_code_str(
session: AsyncSession, code_str: str) -> Optional[PromoCode]:
stmt = select(PromoCode).where(
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)))
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))
result = await session.execute(stmt)
return result.scalars().all()
async def increment_promo_code_usage(
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:
promo.current_activations += 1
await session.flush()
await session.refresh(promo)
return promo
else:
logging.warning(
f"Promo code {promo.code} (ID: {promo_code_id}) already reached max activations."
)
return None
return None
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)
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)
if existing_activation:
logging.info(
f"User {user_id} has already activated promo code {promo_code_id}. Activation ID: {existing_activation.activation_id}"
)
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:
logging.error(
f"Cannot record promo activation: User {user_id} or Promo {promo_code_id} not found."
)
return None
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."
)
return None
activation_data = {
"promo_code_id": promo_code_id,
"user_id": user_id,
"payment_id": payment_id,
"activated_at": datetime.now(timezone.utc)
}
new_activation = PromoCodeActivation(**activation_data)
session.add(new_activation)
await session.flush()
await session.refresh(new_activation)
logging.info(
f"Promo code {promo_code_id} activated by user {user_id}. Activation ID: {new_activation.activation_id}"
)
return new_activation
+205
View File
@@ -0,0 +1,205 @@
import logging
from typing import Optional, List, Dict, Any
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
async def get_active_subscription_by_user_id(
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,
Subscription.end_date > datetime.now(timezone.utc))
if panel_user_uuid:
stmt = stmt.where(Subscription.panel_user_uuid == panel_user_uuid)
stmt = stmt.order_by(Subscription.end_date.desc())
result = await session.execute(stmt)
return result.scalar_one_or_none()
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)
result = await session.execute(stmt)
return result.scalar_one_or_none()
async def create_subscription(session: AsyncSession,
sub_data: Dict[str, Any]) -> Subscription:
from .user_dal import get_user_by_id
if "user_id" not in sub_data or sub_data["user_id"] is None:
raise ValueError(
"user_id is required to create a subscription directly.")
user = await get_user_by_id(session, sub_data["user_id"])
if not user:
raise ValueError(
f"User with id {sub_data['user_id']} not found for creating subscription."
)
new_sub = Subscription(**sub_data)
session.add(new_sub)
await session.flush()
await session.refresh(new_sub)
logging.info(
f"Subscription {new_sub.subscription_id} created for user {new_sub.user_id}"
)
return new_sub
async def update_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():
setattr(sub, key, value)
await session.flush()
await session.refresh(sub)
return sub
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)
if existing_sub:
logging.info(
f"Updating existing subscription {existing_sub.subscription_id} by panel_sub_uuid {panel_sub_uuid}"
)
for key, value in sub_payload.items():
if hasattr(existing_sub, key):
setattr(existing_sub, key, value)
await session.flush()
await session.refresh(existing_sub)
return existing_sub
else:
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 "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(
f"User {sub_payload['user_id']} not found for new subscription with panel_uuid {panel_sub_uuid}."
)
new_sub = Subscription(**sub_payload)
session.add(new_sub)
await session.flush()
await session.refresh(new_sub)
return new_sub
async def deactivate_other_active_subscriptions(
session: AsyncSession, panel_user_uuid: str,
current_panel_subscription_uuid: Optional[str]):
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)
result = await session.execute(stmt)
if result.rowcount > 0:
logging.info(
f"Deactivated {result.rowcount} other active subscriptions for panel_user_uuid {panel_user_uuid}."
)
async def update_subscription_end_date(
session: AsyncSession, subscription_id: int,
new_end_date: datetime) -> Optional[Subscription]:
return await update_subscription(
session, subscription_id, {
"end_date": new_end_date,
"last_notification_sent": None,
"is_active": True,
"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)
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]:
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.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]:
return await update_subscription(
session, subscription_id,
{"last_notification_sent": notification_time})
async def get_user_active_subscription_end_date_str(
session: AsyncSession, user_id: int) -> Optional[str]:
stmt = (select(Subscription.end_date).where(
Subscription.user_id == user_id, Subscription.is_active == True,
Subscription.end_date > datetime.now(timezone.utc)).order_by(
Subscription.end_date.desc()).limit(1))
result = await session.execute(stmt)
end_date_obj = result.scalar_one_or_none()
return end_date_obj.strftime('%Y-%m-%d') if end_date_obj else None
async def find_subscription_for_notification_update(
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)
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()
+124
View File
@@ -0,0 +1,124 @@
import logging
from typing import Optional, List, Dict, Any, Tuple
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from sqlalchemy.orm import selectinload
from sqlalchemy import update, delete, func, and_
from datetime import datetime
from ..models import User, Subscription
async def get_user_by_id(session: AsyncSession,
user_id: int) -> Optional[User]:
stmt = select(User).where(User.user_id == user_id)
result = await session.execute(stmt)
return result.scalar_one_or_none()
async def get_user_by_username(session: AsyncSession,
username: str) -> Optional[User]:
clean_username = username.lstrip('@').lower()
stmt = select(User).where(func.lower(User.username) == clean_username)
result = await session.execute(stmt)
return result.scalar_one_or_none()
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()
async def create_user(session: AsyncSession, user_data: Dict[str,
Any]) -> User:
if 'registration_date' not in user_data:
user_data['registration_date'] = datetime.now()
new_user = User(**user_data)
session.add(new_user)
await session.flush()
await session.refresh(new_user)
logging.info(
f"New user {new_user.user_id} created in DAL. Referred by: {new_user.referred_by_id or 'N/A'}."
)
return new_user
async def update_user(session: AsyncSession, user_id: int,
update_data: Dict[str, Any]) -> Optional[User]:
user = await get_user_by_id(session, user_id)
if user:
for key, value in update_data.items():
setattr(user, key, value)
await session.flush()
await session.refresh(user)
return user
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
async def set_user_ban_status(session: AsyncSession, user_id: int,
is_banned: bool) -> bool:
user = await get_user_by_id(session, user_id)
if user:
user.is_banned = is_banned
await session.flush()
await session.refresh(user)
return True
return False
async def get_banned_users_paginated(session: AsyncSession, limit: int,
offset: int) -> Tuple[List[User], int]:
stmt_users = select(User).where(User.is_banned == True).order_by(
User.registration_date.desc()).limit(limit).offset(offset)
result_users = await session.execute(stmt_users)
users_list = result_users.scalars().all()
stmt_count = select(
func.count()).select_from(User).where(User.is_banned == True)
result_count = await session.execute(stmt_count)
total_banned = result_count.scalar_one()
return users_list, total_banned
async def get_all_active_user_ids_for_broadcast(
session: AsyncSession) -> List[int]:
stmt = select(User.user_id).where(User.is_banned == False)
result = await session.execute(stmt)
return result.scalars().all()
async def get_user_count_stats_dal(session: AsyncSession) -> Dict[str, int]:
total_users_stmt = select(func.count(User.user_id)).select_from(User)
banned_users_stmt = select(func.count(
User.user_id)).select_from(User).where(User.is_banned == True)
active_subs_stmt = (select(func.count(
func.distinct(Subscription.user_id))).join(
User, Subscription.user_id == User.user_id).where(
Subscription.is_active == True).where(
Subscription.end_date > datetime.now()))
total_users = (await
session.execute(total_users_stmt)).scalar_one_or_none() or 0
banned_users = (
await session.execute(banned_users_stmt)).scalar_one_or_none() or 0
active_subs_users = (
await session.execute(active_subs_stmt)).scalar_one_or_none() or 0
return {
"total_users": total_users,
"banned_users": banned_users,
"users_with_active_subscriptions": active_subs_users,
}