fix: reset trial eligibility from web admin
This commit is contained in:
@@ -1172,10 +1172,6 @@ async def admin_user_reset_trial_route(request: web.Request) -> web.Response:
|
||||
actor_id = _require_admin_user_id(request)
|
||||
target_id = int(request.match_info["user_id"])
|
||||
settings: Settings = request.app["settings"]
|
||||
panel_service = request.app.get("panel_service")
|
||||
subscription_service = request.app.get("subscription_service")
|
||||
if panel_service is None or subscription_service is None:
|
||||
return _error(503, "service_unavailable")
|
||||
|
||||
async_session_factory: sessionmaker = request.app["async_session_factory"]
|
||||
async with async_session_factory() as session:
|
||||
@@ -1183,16 +1179,17 @@ async def admin_user_reset_trial_route(request: web.Request) -> web.Response:
|
||||
if not user:
|
||||
return _error(404, "not_found")
|
||||
|
||||
active = await subscription_dal.get_active_subscription_by_user_id(session, target_id)
|
||||
if active:
|
||||
await session.delete(active)
|
||||
reset_at = await user_dal.mark_trial_eligibility_reset(session, target_id)
|
||||
if reset_at is None:
|
||||
await session.rollback()
|
||||
return _error(404, "not_found")
|
||||
|
||||
await message_log_dal.create_message_log(
|
||||
await message_log_dal.create_message_log_no_commit(
|
||||
session,
|
||||
{
|
||||
"user_id": actor_id,
|
||||
"event_type": "admin_reset_trial_webapp",
|
||||
"content": f"Reset trial for user_id={target_id}",
|
||||
"content": f"Reset trial eligibility for user_id={target_id}",
|
||||
"is_admin_event": True,
|
||||
"target_user_id": target_id,
|
||||
},
|
||||
|
||||
@@ -67,7 +67,7 @@ async def _build_user_payload(request: web.Request, user_id: int) -> Dict[str, A
|
||||
trial_available = bool(
|
||||
settings.TRIAL_ENABLED
|
||||
and settings.TRIAL_DURATION_DAYS > 0
|
||||
and not await subscription_service.has_had_any_subscription(session, user_id)
|
||||
and not await subscription_service.has_trial_blocking_subscription(session, user_id)
|
||||
)
|
||||
avatar = await _ensure_cached_telegram_avatar(request, session, db_user)
|
||||
try:
|
||||
|
||||
@@ -932,8 +932,7 @@ async def handle_reset_trial(
|
||||
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
||||
|
||||
try:
|
||||
# Delete all user subscriptions to reset trial eligibility
|
||||
await subscription_dal.delete_all_user_subscriptions(session, user.user_id)
|
||||
await user_dal.mark_trial_eligibility_reset(session, user.user_id)
|
||||
await session.commit()
|
||||
|
||||
await callback.answer(_("admin_user_trial_reset_success"), show_alert=True)
|
||||
|
||||
@@ -45,12 +45,12 @@ async def should_show_trial_button(
|
||||
if not settings.TRIAL_ENABLED:
|
||||
return False
|
||||
|
||||
if hasattr(subscription_service, "has_had_any_subscription") and callable(
|
||||
getattr(subscription_service, "has_had_any_subscription")
|
||||
if hasattr(subscription_service, "has_trial_blocking_subscription") and callable(
|
||||
getattr(subscription_service, "has_trial_blocking_subscription")
|
||||
):
|
||||
return not await subscription_service.has_had_any_subscription(session, user_id)
|
||||
return not await subscription_service.has_trial_blocking_subscription(session, user_id)
|
||||
|
||||
logging.error("Method has_had_any_subscription is missing in SubscriptionService!")
|
||||
logging.error("Method has_trial_blocking_subscription is missing in SubscriptionService!")
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ async def request_trial_confirmation_handler(
|
||||
return
|
||||
|
||||
if settings.TRIAL_ENABLED:
|
||||
if not await subscription_service.has_had_any_subscription(session, user_id):
|
||||
if not await subscription_service.has_trial_blocking_subscription(session, user_id):
|
||||
pass
|
||||
|
||||
if not settings.TRIAL_ENABLED:
|
||||
@@ -60,7 +60,7 @@ async def request_trial_confirmation_handler(
|
||||
pass
|
||||
return
|
||||
|
||||
if await subscription_service.has_had_any_subscription(session, user_id):
|
||||
if await subscription_service.has_trial_blocking_subscription(session, user_id):
|
||||
await callback.message.edit_text(
|
||||
_("trial_already_had_subscription_or_trial"),
|
||||
reply_markup=get_main_menu_inline_keyboard(current_lang, i18n, settings, False),
|
||||
@@ -147,8 +147,9 @@ async def request_trial_confirmation_handler(
|
||||
await callback.answer(final_message_text_in_chat, show_alert=True)
|
||||
except Exception:
|
||||
pass
|
||||
if settings.TRIAL_ENABLED and not await subscription_service.has_had_any_subscription(
|
||||
session, user_id
|
||||
if (
|
||||
settings.TRIAL_ENABLED
|
||||
and not await subscription_service.has_trial_blocking_subscription(session, user_id)
|
||||
):
|
||||
show_trial_button_after_action = True
|
||||
|
||||
@@ -218,7 +219,7 @@ async def confirm_activate_trial_handler(
|
||||
callback, settings, i18n_data, subscription_service, session, is_edit=True
|
||||
)
|
||||
return
|
||||
if await subscription_service.has_had_any_subscription(session, user_id):
|
||||
if await subscription_service.has_trial_blocking_subscription(session, user_id):
|
||||
try:
|
||||
await callback.answer(_("trial_already_had_subscription_or_trial"), show_alert=True)
|
||||
except Exception:
|
||||
@@ -283,8 +284,9 @@ async def confirm_activate_trial_handler(
|
||||
await callback.answer(final_message_text_in_chat, show_alert=True)
|
||||
except Exception:
|
||||
pass
|
||||
if settings.TRIAL_ENABLED and not await subscription_service.has_had_any_subscription(
|
||||
session, user_id
|
||||
if (
|
||||
settings.TRIAL_ENABLED
|
||||
and not await subscription_service.has_trial_blocking_subscription(session, user_id)
|
||||
):
|
||||
show_trial_button_after_action = True
|
||||
|
||||
|
||||
@@ -62,6 +62,9 @@ class PaymentContextMixin:
|
||||
async def has_had_any_subscription(self, session: AsyncSession, user_id: int) -> bool:
|
||||
return await subscription_dal.has_any_subscription_for_user(session, user_id)
|
||||
|
||||
async def has_trial_blocking_subscription(self, session: AsyncSession, user_id: int) -> bool:
|
||||
return await subscription_dal.has_trial_blocking_subscription_for_user(session, user_id)
|
||||
|
||||
async def has_active_subscription(self, session: AsyncSession, user_id: int) -> bool:
|
||||
"""Return True if user currently has an active subscription (end_date in future)."""
|
||||
try:
|
||||
|
||||
@@ -22,7 +22,7 @@ class TrialSubscriptionMixin:
|
||||
"message_key": "user_not_found_for_trial",
|
||||
}
|
||||
|
||||
if await self.has_had_any_subscription(session, user_id):
|
||||
if await self.has_trial_blocking_subscription(session, user_id):
|
||||
return {
|
||||
"eligible": False,
|
||||
"activated": False,
|
||||
|
||||
@@ -4,12 +4,12 @@ import secrets
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from sqlalchemy import delete, func, or_, update
|
||||
from sqlalchemy import and_, delete, func, or_, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from db.models import Subscription, SubscriptionNotification
|
||||
from db.models import Subscription, SubscriptionNotification, User
|
||||
|
||||
INSTALL_SHARE_TOKEN_BYTES = 16
|
||||
|
||||
@@ -252,7 +252,7 @@ async def deactivate_all_user_subscriptions(session: AsyncSession, user_id: int)
|
||||
|
||||
|
||||
async def delete_all_user_subscriptions(session: AsyncSession, user_id: int) -> int:
|
||||
"""Completely delete all user subscriptions (for trial reset)"""
|
||||
"""Completely delete all user subscriptions."""
|
||||
stmt = delete(Subscription).where(Subscription.user_id == user_id)
|
||||
result = await session.execute(stmt)
|
||||
if result.rowcount > 0:
|
||||
@@ -284,6 +284,28 @@ async def has_any_subscription_for_user(session: AsyncSession, user_id: int) ->
|
||||
return result.scalar_one_or_none() is not None
|
||||
|
||||
|
||||
async def has_trial_blocking_subscription_for_user(session: AsyncSession, user_id: int) -> bool:
|
||||
now_utc = datetime.now(timezone.utc)
|
||||
reset_at = (
|
||||
select(User.trial_eligibility_reset_at).where(User.user_id == user_id).scalar_subquery()
|
||||
)
|
||||
subscription_anchor = func.coalesce(Subscription.start_date, Subscription.end_date)
|
||||
stmt = (
|
||||
select(Subscription.subscription_id)
|
||||
.where(
|
||||
Subscription.user_id == user_id,
|
||||
or_(
|
||||
reset_at.is_(None),
|
||||
and_(Subscription.is_active == True, Subscription.end_date > now_utc),
|
||||
subscription_anchor > reset_at,
|
||||
),
|
||||
)
|
||||
.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]:
|
||||
|
||||
@@ -261,6 +261,20 @@ async def create_email_user(
|
||||
)
|
||||
|
||||
|
||||
async def mark_trial_eligibility_reset(
|
||||
session: AsyncSession,
|
||||
user_id: int,
|
||||
*,
|
||||
reset_at: Optional[datetime] = None,
|
||||
) -> Optional[datetime]:
|
||||
reset_at = reset_at or datetime.now(timezone.utc)
|
||||
stmt = update(User).where(User.user_id == user_id).values(trial_eligibility_reset_at=reset_at)
|
||||
result = await session.execute(stmt)
|
||||
if result.rowcount <= 0:
|
||||
return None
|
||||
return reset_at
|
||||
|
||||
|
||||
async def _has_active_panel_subscription(
|
||||
session: AsyncSession, user_id: int, panel_user_uuid: str
|
||||
) -> bool:
|
||||
|
||||
@@ -1061,6 +1061,15 @@ def _migration_0032_add_telegram_notification_status(connection: Connection) ->
|
||||
connection.execute(text(f"ALTER TABLE users ADD COLUMN {column} {ddl_type}"))
|
||||
|
||||
|
||||
def _migration_0033_add_trial_eligibility_reset_marker(connection: Connection) -> None:
|
||||
inspector = inspect(connection)
|
||||
columns: Set[str] = {col["name"] for col in inspector.get_columns("users")}
|
||||
if "trial_eligibility_reset_at" not in columns:
|
||||
connection.execute(
|
||||
text("ALTER TABLE users ADD COLUMN trial_eligibility_reset_at TIMESTAMPTZ")
|
||||
)
|
||||
|
||||
|
||||
MIGRATIONS: List[Migration] = [
|
||||
Migration(
|
||||
id="0001_add_channel_subscription_fields",
|
||||
@@ -1233,6 +1242,11 @@ MIGRATIONS: List[Migration] = [
|
||||
description="Track whether the bot can message Telegram-linked users",
|
||||
upgrade=_migration_0032_add_telegram_notification_status,
|
||||
),
|
||||
Migration(
|
||||
id="0033_add_trial_eligibility_reset_marker",
|
||||
description="Track admin resets of per-user trial eligibility without deleting history",
|
||||
upgrade=_migration_0033_add_trial_eligibility_reset_marker,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ class User(Base):
|
||||
referred_by_id = Column(BigInteger, ForeignKey("users.user_id"), nullable=True)
|
||||
lifetime_used_traffic_bytes = Column(BigInteger, nullable=True)
|
||||
lifetime_used_traffic_synced_at = Column(DateTime(timezone=True), nullable=True)
|
||||
trial_eligibility_reset_at = Column(DateTime(timezone=True), nullable=True)
|
||||
channel_subscription_verified = Column(Boolean, nullable=True)
|
||||
channel_subscription_checked_at = Column(DateTime(timezone=True), nullable=True)
|
||||
channel_subscription_verified_for = Column(BigInteger, nullable=True)
|
||||
|
||||
Reference in New Issue
Block a user