chore: run lint and prettifier
This commit is contained in:
+12
-12
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user