671 lines
34 KiB
Python
671 lines
34 KiB
Python
import logging
|
|
import aiosqlite
|
|
from datetime import datetime, timedelta, timezone
|
|
from typing import Optional, Dict, Any, List, Callable, Awaitable
|
|
|
|
from config.settings import Settings
|
|
from db.database import get_db_connection_manager, _setup_db_connection, get_user, has_had_any_subscription as db_has_had_any_subscription
|
|
from .panel_api_service import PanelApiService
|
|
|
|
if False:
|
|
from .referral_service import ReferralService
|
|
|
|
|
|
class SubscriptionService:
|
|
|
|
def __init__(self, db_conn_provider: Callable[[], Any], settings: Settings,
|
|
panel_service: PanelApiService):
|
|
self.db_conn_provider = db_conn_provider
|
|
self.settings = settings
|
|
self.panel_service = panel_service
|
|
|
|
async def _get_db(
|
|
self,
|
|
existing_conn: Optional[aiosqlite.Connection] = None
|
|
) -> tuple[aiosqlite.Connection, bool]:
|
|
"""Helper to get a DB connection. Returns (connection, should_manage_flag)."""
|
|
if existing_conn:
|
|
return existing_conn, False
|
|
|
|
conn_manager = self.db_conn_provider()
|
|
conn = await conn_manager.__aenter__()
|
|
try:
|
|
await _setup_db_connection(conn)
|
|
except Exception as e:
|
|
await conn_manager.__aexit__(type(e), e, e.__traceback__)
|
|
raise
|
|
return conn, True
|
|
|
|
async def _release_db(self,
|
|
db: aiosqlite.Connection,
|
|
should_manage: bool,
|
|
exc_type=None,
|
|
exc_val=None,
|
|
exc_tb=None):
|
|
"""Helper to release/close a DB connection if this service instance opened it."""
|
|
if should_manage:
|
|
await db.__aexit__(exc_type, exc_val, exc_tb)
|
|
|
|
async def get_user_language(self, user_id: int) -> str:
|
|
"""Fetches the user's language preference from the local database."""
|
|
db, should_manage = await self._get_db()
|
|
try:
|
|
user_record = await get_user(user_id, db_conn=db)
|
|
default_lang = self.settings.DEFAULT_LANGUAGE
|
|
return user_record[
|
|
'language_code'] if user_record and 'language_code' in user_record.keys(
|
|
) and user_record['language_code'] else default_lang
|
|
finally:
|
|
await self._release_db(db, should_manage)
|
|
|
|
async def get_panel_user_uuid(
|
|
self,
|
|
user_id: int,
|
|
db_conn: Optional[aiosqlite.Connection] = None) -> Optional[str]:
|
|
"""Fetches panel_user_uuid for a given Telegram user_id from local DB."""
|
|
db_to_use, should_manage_this_conn = await self._get_db(db_conn)
|
|
try:
|
|
user_record = await get_user(user_id, db_conn=db_to_use)
|
|
return user_record[
|
|
'panel_user_uuid'] if user_record and 'panel_user_uuid' in user_record.keys(
|
|
) and user_record['panel_user_uuid'] else None
|
|
finally:
|
|
await self._release_db(db_to_use, should_manage_this_conn)
|
|
|
|
async def has_had_any_subscription(
|
|
self,
|
|
user_id: int,
|
|
db_conn: Optional[aiosqlite.Connection] = None) -> bool:
|
|
"""Checks if the user has any record in the subscriptions table."""
|
|
db_to_use, should_manage_this_conn = await self._get_db(db_conn)
|
|
try:
|
|
return await db_has_had_any_subscription(user_id,
|
|
db_conn=db_to_use)
|
|
finally:
|
|
await self._release_db(db_to_use, should_manage_this_conn)
|
|
|
|
async def activate_trial_subscription(
|
|
self, user_id: int) -> Optional[Dict[str, Any]]:
|
|
"""
|
|
Activates a trial subscription for an eligible user.
|
|
Manages its own database connection and transaction.
|
|
Returns dict with trial details or specific error dict on failure/ineligibility.
|
|
"""
|
|
if not self.settings.TRIAL_ENABLED or self.settings.TRIAL_DURATION_DAYS <= 0:
|
|
logging.info(
|
|
f"Trial subscription feature is disabled or duration invalid for user {user_id}."
|
|
)
|
|
return {
|
|
"eligible": False,
|
|
"activated": False,
|
|
"message_key": "trial_feature_disabled"
|
|
}
|
|
|
|
async with self.db_conn_provider() as db:
|
|
await _setup_db_connection(db)
|
|
try:
|
|
if await db_has_had_any_subscription(user_id, db_conn=db):
|
|
logging.info(
|
|
f"User {user_id} has prior subscriptions. Trial not applicable."
|
|
)
|
|
return {
|
|
"eligible": False,
|
|
"activated": False,
|
|
"message_key": "trial_not_eligible_already_subscribed"
|
|
}
|
|
|
|
panel_user_uuid = await self.get_panel_user_uuid(user_id,
|
|
db_conn=db)
|
|
panel_subscription_uuid_for_link = None
|
|
panel_short_uuid_for_link = None
|
|
panel_user_interacted_now = False
|
|
specific_inbounds = self.settings.parsed_default_panel_user_inbound_uuids
|
|
panel_actual_subscription_url: Optional[str] = None
|
|
|
|
if not panel_user_uuid:
|
|
logging.info(
|
|
f"No panel_user_uuid for TG user_id {user_id} for trial. Creating panel user."
|
|
)
|
|
panel_username_to_create = f"tg_{user_id}"
|
|
creation_response = await self.panel_service.create_panel_user(
|
|
username=panel_username_to_create,
|
|
telegram_id=user_id,
|
|
default_expire_days=self.settings.TRIAL_DURATION_DAYS,
|
|
default_traffic_limit_bytes=self.settings.
|
|
trial_traffic_limit_bytes,
|
|
default_traffic_limit_strategy=self.settings.
|
|
PANEL_USER_DEFAULT_TRAFFIC_STRATEGY,
|
|
specific_inbound_uuids=specific_inbounds,
|
|
activate_all_inbounds_default_flag=False
|
|
if specific_inbounds else True)
|
|
panel_user_obj_from_api = None
|
|
if creation_response and not creation_response.get(
|
|
"error"):
|
|
panel_user_obj_from_api = creation_response.get(
|
|
"response")
|
|
elif creation_response and creation_response.get(
|
|
"errorCode") == "A019":
|
|
existing_users_list = await self.panel_service.get_users_by_filter(
|
|
username=panel_username_to_create)
|
|
if existing_users_list and len(
|
|
existing_users_list) == 1:
|
|
panel_user_obj_from_api = existing_users_list[0]
|
|
|
|
if panel_user_obj_from_api and panel_user_obj_from_api.get(
|
|
'uuid'):
|
|
panel_user_uuid = panel_user_obj_from_api['uuid']
|
|
panel_subscription_uuid_for_link = panel_user_obj_from_api.get(
|
|
'subscriptionUuid')
|
|
panel_short_uuid_for_link = panel_user_obj_from_api.get(
|
|
'shortUuid')
|
|
panel_actual_subscription_url = panel_user_obj_from_api.get(
|
|
'subscriptionUrl')
|
|
await db.execute(
|
|
"UPDATE users SET panel_user_uuid = ? WHERE user_id = ?",
|
|
(panel_user_uuid, user_id))
|
|
panel_user_interacted_now = True
|
|
if panel_user_obj_from_api.get(
|
|
'telegramId') != user_id:
|
|
await self.panel_service.update_user_details_on_panel(
|
|
panel_user_uuid, {"telegramId": user_id})
|
|
else:
|
|
logging.error(
|
|
f"Failed to create/link panel user for trial (TG_ID {user_id}). Resp: {creation_response if 'creation_response' in locals() else 'N/A'}"
|
|
)
|
|
await db.rollback()
|
|
return {
|
|
"eligible": True,
|
|
"activated": False,
|
|
"message_key": "trial_activation_failed"
|
|
}
|
|
else:
|
|
panel_user_data = await self.panel_service.get_user_by_uuid(
|
|
panel_user_uuid)
|
|
if panel_user_data:
|
|
panel_subscription_uuid_for_link = panel_user_data.get(
|
|
'subscriptionUuid')
|
|
panel_short_uuid_for_link = panel_user_data.get(
|
|
'shortUuid')
|
|
panel_actual_subscription_url = panel_user_data.get(
|
|
'subscriptionUrl')
|
|
|
|
if not panel_subscription_uuid_for_link and panel_short_uuid_for_link:
|
|
panel_subscription_uuid_for_link = panel_short_uuid_for_link
|
|
if not panel_subscription_uuid_for_link:
|
|
logging.error(
|
|
f"Critical: panel_subscription_uuid for link is None for trial (panel_uuid {panel_user_uuid})."
|
|
)
|
|
await db.rollback()
|
|
return {
|
|
"eligible": True,
|
|
"activated": False,
|
|
"message_key": "trial_activation_failed"
|
|
}
|
|
|
|
start_date = datetime.now(timezone.utc)
|
|
end_date = start_date + timedelta(
|
|
days=self.settings.TRIAL_DURATION_DAYS)
|
|
await db.execute(
|
|
"UPDATE subscriptions SET is_active = 0 WHERE panel_user_uuid = ? AND is_active = 1",
|
|
(panel_user_uuid, ))
|
|
upsert_sql = """ INSERT INTO subscriptions (user_id, panel_user_uuid, panel_subscription_uuid, start_date, end_date, duration_months, is_active, status_from_panel, traffic_limit_bytes) VALUES (?, ?, ?, ?, ?, 0, 1, 'TRIAL', ?) ON CONFLICT(panel_subscription_uuid) DO UPDATE SET user_id = excluded.user_id, panel_user_uuid = excluded.panel_user_uuid, start_date = excluded.start_date, end_date = excluded.end_date, duration_months = 0, is_active = 1, status_from_panel = 'TRIAL', traffic_limit_bytes = excluded.traffic_limit_bytes, last_notification_sent = NULL; """
|
|
trial_traffic_val = self.settings.trial_traffic_limit_bytes
|
|
params = (user_id, panel_user_uuid,
|
|
panel_subscription_uuid_for_link,
|
|
start_date.isoformat(), end_date.isoformat(),
|
|
trial_traffic_val)
|
|
|
|
upsert_cursor = await db.execute(upsert_sql, params)
|
|
trial_subscription_id = upsert_cursor.lastrowid
|
|
if not trial_subscription_id or trial_subscription_id == 0:
|
|
id_fetch_cursor = await db.execute(
|
|
"SELECT subscription_id FROM subscriptions WHERE panel_subscription_uuid = ?",
|
|
(panel_subscription_uuid_for_link, ))
|
|
id_fetch_row = await id_fetch_cursor.fetchone()
|
|
if id_fetch_cursor: await id_fetch_cursor.close()
|
|
if id_fetch_row:
|
|
trial_subscription_id = id_fetch_row['subscription_id']
|
|
logging.info(
|
|
f"Local trial subscription (ID: {trial_subscription_id}) for user {user_id} prepared. Ends: {end_date.isoformat()}."
|
|
)
|
|
|
|
panel_update_payload: Dict[str, Any] = {
|
|
"uuid":
|
|
panel_user_uuid,
|
|
"expireAt":
|
|
end_date.isoformat(timespec='milliseconds').replace(
|
|
'+00:00', 'Z'),
|
|
"status":
|
|
"ACTIVE",
|
|
"trafficLimitBytes":
|
|
trial_traffic_val,
|
|
"trafficLimitStrategy":
|
|
self.settings.PANEL_USER_DEFAULT_TRAFFIC_STRATEGY,
|
|
}
|
|
if specific_inbounds:
|
|
panel_update_payload[
|
|
"activeUserInbounds"] = specific_inbounds
|
|
elif panel_user_interacted_now:
|
|
panel_update_payload["activateAllInbounds"] = True
|
|
|
|
updated_panel_user = await self.panel_service.update_user_details_on_panel(
|
|
panel_user_uuid, panel_update_payload)
|
|
if not updated_panel_user:
|
|
logging.warning(
|
|
f"Panel user details update FAILED for trial user {panel_user_uuid}."
|
|
)
|
|
else:
|
|
logging.info(
|
|
f"Panel user {panel_user_uuid} details updated for trial. Panel ExpireAt: {updated_panel_user.get('expireAt')}"
|
|
)
|
|
if updated_panel_user.get('subscriptionUrl'):
|
|
panel_actual_subscription_url = updated_panel_user.get(
|
|
'subscriptionUrl')
|
|
if updated_panel_user.get('shortUuid'):
|
|
panel_short_uuid_for_link = updated_panel_user.get(
|
|
'shortUuid')
|
|
|
|
await db.commit()
|
|
return {
|
|
"eligible": True,
|
|
"activated": True,
|
|
"end_date": end_date,
|
|
"days": self.settings.TRIAL_DURATION_DAYS,
|
|
"traffic_gb": self.settings.TRIAL_TRAFFIC_LIMIT_GB,
|
|
"panel_user_uuid": panel_user_uuid,
|
|
"panel_short_uuid": panel_short_uuid_for_link,
|
|
"subscription_url": panel_actual_subscription_url
|
|
}
|
|
except Exception as e:
|
|
logging.error(
|
|
f"Error activating trial for user {user_id}: {e}",
|
|
exc_info=True)
|
|
await db.rollback()
|
|
return {
|
|
"eligible": True,
|
|
"activated": False,
|
|
"message_key": "trial_activation_failed"
|
|
}
|
|
|
|
async def activate_subscription(
|
|
self,
|
|
user_id: int,
|
|
months: int,
|
|
payment_amount: float,
|
|
payment_id_internal: int,
|
|
db_conn: aiosqlite.Connection,
|
|
promo_code_id: Optional[int] = None) -> Optional[Dict[str, Any]]:
|
|
db = db_conn
|
|
try:
|
|
panel_user_uuid = await self.get_panel_user_uuid(user_id,
|
|
db_conn=db)
|
|
panel_subscription_uuid_for_link = None
|
|
panel_short_uuid_for_link = None
|
|
panel_user_interacted_now = False
|
|
panel_actual_subscription_url: Optional[str] = None
|
|
specific_inbounds_from_settings = self.settings.parsed_default_panel_user_inbound_uuids
|
|
if not panel_user_uuid:
|
|
panel_username_to_create = f"tg_{user_id}"
|
|
creation_response = await self.panel_service.create_panel_user(
|
|
username=panel_username_to_create,
|
|
telegram_id=user_id,
|
|
default_expire_days=self.settings.
|
|
PANEL_USER_DEFAULT_EXPIRE_DAYS,
|
|
default_traffic_limit_bytes=self.settings.
|
|
PANEL_USER_DEFAULT_TRAFFIC_BYTES,
|
|
default_traffic_limit_strategy=self.settings.
|
|
PANEL_USER_DEFAULT_TRAFFIC_STRATEGY,
|
|
specific_inbound_uuids=specific_inbounds_from_settings,
|
|
activate_all_inbounds_default_flag=False
|
|
if specific_inbounds_from_settings else True)
|
|
panel_user_object_from_api = None
|
|
if creation_response and not creation_response.get("error"):
|
|
panel_user_object_from_api = creation_response.get(
|
|
"response")
|
|
elif creation_response and creation_response.get(
|
|
"errorCode") == "A019":
|
|
existing_users_list = await self.panel_service.get_users_by_filter(
|
|
username=panel_username_to_create)
|
|
if existing_users_list and len(existing_users_list) == 1:
|
|
panel_user_object_from_api = existing_users_list[0]
|
|
if panel_user_object_from_api and panel_user_object_from_api.get(
|
|
'uuid'):
|
|
panel_user_uuid = panel_user_object_from_api['uuid']
|
|
panel_subscription_uuid_for_link = panel_user_object_from_api.get(
|
|
'subscriptionUuid')
|
|
panel_short_uuid_for_link = panel_user_object_from_api.get(
|
|
'shortUuid')
|
|
panel_actual_subscription_url = panel_user_object_from_api.get(
|
|
'subscriptionUrl')
|
|
cursor_conflict = await db.execute(
|
|
"SELECT user_id FROM users WHERE panel_user_uuid = ? AND user_id != ?",
|
|
(panel_user_uuid, user_id))
|
|
conflicting_tg_user = await cursor_conflict.fetchone()
|
|
await cursor_conflict.close()
|
|
if conflicting_tg_user:
|
|
logging.error(
|
|
f"CRITICAL CONFLICT: Panel UUID {panel_user_uuid} already linked to TG user {conflicting_tg_user['user_id']}."
|
|
)
|
|
return None
|
|
await db.execute(
|
|
"UPDATE users SET panel_user_uuid = ? WHERE user_id = ?",
|
|
(panel_user_uuid, user_id))
|
|
panel_user_interacted_now = True
|
|
if panel_user_object_from_api.get('telegramId') != user_id:
|
|
await self.panel_service.update_user_details_on_panel(
|
|
panel_user_uuid, {"telegramId": user_id})
|
|
else:
|
|
logging.error(
|
|
f"Failed to create/link panel user for TG_ID {user_id}. Resp: {creation_response if 'creation_response' in locals() else 'N/A'}"
|
|
)
|
|
return None
|
|
else:
|
|
panel_user_data = await self.panel_service.get_user_by_uuid(
|
|
panel_user_uuid)
|
|
if panel_user_data:
|
|
panel_subscription_uuid_for_link = panel_user_data.get(
|
|
'subscriptionUuid')
|
|
panel_short_uuid_for_link = panel_user_data.get(
|
|
'shortUuid')
|
|
panel_actual_subscription_url = panel_user_data.get(
|
|
'subscriptionUrl')
|
|
if not panel_subscription_uuid_for_link and panel_short_uuid_for_link:
|
|
panel_subscription_uuid_for_link = panel_short_uuid_for_link
|
|
if not panel_subscription_uuid_for_link:
|
|
logging.error(
|
|
f"Critical: panel_subscription_uuid for link is None for panel_user_uuid {panel_user_uuid}."
|
|
)
|
|
return None
|
|
|
|
cursor = await db.execute(
|
|
"SELECT subscription_id, end_date FROM subscriptions WHERE panel_user_uuid = ? AND is_active = 1 ORDER BY end_date DESC LIMIT 1",
|
|
(panel_user_uuid, ))
|
|
current_sub_row = await cursor.fetchone()
|
|
await cursor.close()
|
|
current_sub_end_date_str = current_sub_row[
|
|
'end_date'] if current_sub_row else None
|
|
start_date = datetime.now(timezone.utc)
|
|
if current_sub_end_date_str:
|
|
try:
|
|
parsed_current_end_date = datetime.fromisoformat(
|
|
current_sub_end_date_str.replace("Z", "+00:00"))
|
|
if parsed_current_end_date.tzinfo is None:
|
|
parsed_current_end_date = parsed_current_end_date.replace(
|
|
tzinfo=timezone.utc)
|
|
if parsed_current_end_date > start_date:
|
|
start_date = parsed_current_end_date
|
|
except ValueError:
|
|
logging.warning(
|
|
f"Bad current_sub_end_date string: {current_sub_end_date_str} for panel_user {panel_user_uuid}."
|
|
)
|
|
final_end_date = start_date + timedelta(days=months * 30)
|
|
if promo_code_id:
|
|
promo_cursor = await db.execute(
|
|
"SELECT bonus_days FROM promo_codes WHERE promo_code_id = ?",
|
|
(promo_code_id, ))
|
|
promo_row = await promo_cursor.fetchone()
|
|
await promo_cursor.close()
|
|
if promo_row:
|
|
final_end_date += timedelta(days=promo_row['bonus_days'])
|
|
await db.execute(
|
|
"INSERT OR IGNORE INTO promo_code_activations (promo_code_id, user_id, payment_id) VALUES (?, ?, ?)",
|
|
(promo_code_id, user_id, payment_id_internal))
|
|
await db.execute(
|
|
"UPDATE promo_codes SET current_activations = current_activations + 1 WHERE promo_code_id = ?",
|
|
(promo_code_id, ))
|
|
|
|
await db.execute(
|
|
"UPDATE subscriptions SET is_active = 0 WHERE panel_user_uuid = ? AND is_active = 1",
|
|
(panel_user_uuid, ))
|
|
upsert_sql = """ INSERT INTO subscriptions (user_id, panel_user_uuid, panel_subscription_uuid, start_date, end_date, duration_months, is_active, status_from_panel) VALUES (?, ?, ?, ?, ?, ?, 1, 'ACTIVE') ON CONFLICT(panel_subscription_uuid) DO UPDATE SET user_id = excluded.user_id, panel_user_uuid = excluded.panel_user_uuid, start_date = excluded.start_date, end_date = excluded.end_date, duration_months = excluded.duration_months, is_active = 1, status_from_panel = 'ACTIVE', last_notification_sent = NULL; """
|
|
params = (user_id, panel_user_uuid,
|
|
panel_subscription_uuid_for_link, start_date.isoformat(),
|
|
final_end_date.isoformat(), months)
|
|
upsert_cursor = await db.execute(upsert_sql, params)
|
|
subscription_id_to_return = upsert_cursor.lastrowid
|
|
if not subscription_id_to_return or subscription_id_to_return == 0:
|
|
id_cursor = await db.execute(
|
|
"SELECT subscription_id FROM subscriptions WHERE panel_subscription_uuid = ?",
|
|
(panel_subscription_uuid_for_link, ))
|
|
id_row = await id_cursor.fetchone()
|
|
await id_cursor.close()
|
|
if id_row:
|
|
subscription_id_to_return = id_row['subscription_id']
|
|
logging.info(
|
|
f"Local subscription UPSERTED (ID: {subscription_id_to_return}) for user {user_id}. Ends: {final_end_date.isoformat()}."
|
|
)
|
|
|
|
panel_update_payload: Dict[str, Any] = {
|
|
"uuid":
|
|
panel_user_uuid,
|
|
"expireAt":
|
|
final_end_date.isoformat(timespec='milliseconds').replace(
|
|
'+00:00', 'Z'),
|
|
"status":
|
|
"ACTIVE",
|
|
"trafficLimitBytes":
|
|
self.settings.PANEL_USER_DEFAULT_TRAFFIC_BYTES,
|
|
"trafficLimitStrategy":
|
|
self.settings.PANEL_USER_DEFAULT_TRAFFIC_STRATEGY
|
|
}
|
|
if specific_inbounds_from_settings:
|
|
panel_update_payload[
|
|
"activeUserInbounds"] = specific_inbounds_from_settings
|
|
elif panel_user_interacted_now:
|
|
panel_update_payload["activateAllInbounds"] = True
|
|
if "activateAllInbounds" in panel_update_payload and not specific_inbounds_from_settings and not panel_user_interacted_now:
|
|
del panel_update_payload["activateAllInbounds"]
|
|
logging.info(
|
|
f"Attempting to update panel user {panel_user_uuid} for paid sub: {panel_update_payload}"
|
|
)
|
|
updated_panel_user = await self.panel_service.update_user_details_on_panel(
|
|
panel_user_uuid, panel_update_payload)
|
|
if not updated_panel_user:
|
|
logging.warning(
|
|
f"Panel user details update FAILED for {panel_user_uuid}.")
|
|
else:
|
|
logging.info(
|
|
f"Panel user {panel_user_uuid} details updated. Panel ExpireAt: {updated_panel_user.get('expireAt')}"
|
|
)
|
|
if updated_panel_user.get('subscriptionUrl'):
|
|
panel_actual_subscription_url = updated_panel_user.get(
|
|
'subscriptionUrl')
|
|
if updated_panel_user.get('shortUuid'):
|
|
panel_short_uuid_for_link = updated_panel_user.get(
|
|
'shortUuid')
|
|
return {
|
|
"subscription_id": subscription_id_to_return,
|
|
"end_date": final_end_date,
|
|
"is_active": True,
|
|
"panel_user_uuid": panel_user_uuid,
|
|
"panel_short_uuid": panel_short_uuid_for_link,
|
|
"subscription_url": panel_actual_subscription_url
|
|
}
|
|
except Exception as e:
|
|
logging.error(
|
|
f"Error in activate_subscription (paid) for user {user_id}: {e}",
|
|
exc_info=True)
|
|
return None
|
|
|
|
async def extend_active_subscription_days(
|
|
self,
|
|
user_id: int,
|
|
bonus_days: int,
|
|
db_conn: aiosqlite.Connection,
|
|
reason: str = "bonus") -> Optional[datetime]:
|
|
db = db_conn
|
|
try:
|
|
user_cursor = await db.execute(
|
|
"SELECT panel_user_uuid FROM users WHERE user_id = ?",
|
|
(user_id, ))
|
|
user_panel_data = await user_cursor.fetchone()
|
|
if user_cursor: await user_cursor.close()
|
|
panel_user_uuid_for_update = user_panel_data[
|
|
'panel_user_uuid'] if user_panel_data and 'panel_user_uuid' in user_panel_data.keys(
|
|
) and user_panel_data['panel_user_uuid'] else None
|
|
|
|
sql_select_active_sub = "SELECT subscription_id, end_date FROM subscriptions WHERE user_id = ? AND is_active = 1 "
|
|
params_select_active_sub: tuple = (user_id, )
|
|
if panel_user_uuid_for_update:
|
|
sql_select_active_sub += "AND panel_user_uuid = ? "
|
|
params_select_active_sub += (panel_user_uuid_for_update, )
|
|
else:
|
|
logging.warning(
|
|
f"Extending subscription for user {user_id} without panel_user_uuid. This might be ambiguous if user has multiple panel accounts linked to one TG ID (not typical)."
|
|
)
|
|
sql_select_active_sub += "ORDER BY end_date DESC LIMIT 1"
|
|
|
|
cursor = await db.execute(sql_select_active_sub,
|
|
params_select_active_sub)
|
|
active_sub_row = await cursor.fetchone()
|
|
if cursor: await cursor.close()
|
|
|
|
if not active_sub_row or not active_sub_row['end_date']:
|
|
logging.info(
|
|
f"No active subscription found for user {user_id} (panel UUID: {panel_user_uuid_for_update}) to extend with {reason} bonus."
|
|
)
|
|
return None
|
|
|
|
current_end_date_str = active_sub_row['end_date']
|
|
try:
|
|
current_end_date = datetime.fromisoformat(
|
|
current_end_date_str.replace("Z", "+00:00")) if isinstance(
|
|
current_end_date_str, str) else current_end_date_str
|
|
if not isinstance(current_end_date, datetime):
|
|
raise ValueError(
|
|
"current_end_date is not a datetime object after parsing"
|
|
)
|
|
except ValueError as ve:
|
|
logging.error(
|
|
f"Error parsing current_end_date '{current_end_date_str}' for user {user_id}: {ve}"
|
|
)
|
|
return None
|
|
|
|
if current_end_date.tzinfo is None:
|
|
current_end_date = current_end_date.replace(
|
|
tzinfo=timezone.utc)
|
|
|
|
now_utc = datetime.now(timezone.utc)
|
|
start_point_for_bonus = current_end_date if current_end_date > now_utc else now_utc
|
|
new_end_date = start_point_for_bonus + timedelta(days=bonus_days)
|
|
|
|
await db.execute(
|
|
"UPDATE subscriptions SET end_date = ?, last_notification_sent = NULL WHERE subscription_id = ?",
|
|
(new_end_date.isoformat(), active_sub_row['subscription_id']))
|
|
logging.info(
|
|
f"Subscription for user {user_id} extended by {bonus_days} days ({reason}). New end date: {new_end_date.isoformat()}"
|
|
)
|
|
|
|
if panel_user_uuid_for_update:
|
|
panel_update_payload = {
|
|
"uuid":
|
|
panel_user_uuid_for_update,
|
|
"expireAt":
|
|
new_end_date.isoformat(timespec='milliseconds').replace(
|
|
'+00:00', 'Z')
|
|
}
|
|
if not await self.panel_service.update_user_details_on_panel(
|
|
panel_user_uuid_for_update, panel_update_payload):
|
|
logging.warning(
|
|
f"Failed to update panel expiry for {panel_user_uuid_for_update} after {reason} bonus."
|
|
)
|
|
return new_end_date
|
|
except Exception as e:
|
|
logging.error(
|
|
f"Error extending subscription with {reason} bonus for user {user_id} (using provided db_conn): {e}",
|
|
exc_info=True)
|
|
return None
|
|
|
|
async def extend_subscription_for_referral(
|
|
self,
|
|
user_id: int,
|
|
bonus_days: int,
|
|
db_conn: aiosqlite.Connection,
|
|
is_referee_bonus: bool = False) -> Optional[datetime]:
|
|
reason = "referee bonus" if is_referee_bonus else "inviter referral bonus"
|
|
return await self.extend_active_subscription_days(user_id,
|
|
bonus_days,
|
|
db_conn,
|
|
reason=reason)
|
|
|
|
async def get_active_subscription(
|
|
self, user_id: int) -> Optional[Dict[str, Any]]:
|
|
db, should_manage = await self._get_db()
|
|
try:
|
|
now_iso_utc = datetime.now(timezone.utc).isoformat()
|
|
cursor = await db.execute(
|
|
"""SELECT s.subscription_id, s.panel_subscription_uuid, s.panel_user_uuid, s.start_date, s.end_date, s.duration_months, s.is_active, s.status_from_panel, s.traffic_limit_bytes, s.traffic_used_bytes, u.username as bot_username FROM subscriptions s LEFT JOIN users u ON s.user_id = u.user_id WHERE s.user_id = ? AND s.is_active = 1 AND s.end_date > ? ORDER BY s.end_date DESC LIMIT 1""",
|
|
(user_id, now_iso_utc))
|
|
sub_row = await cursor.fetchone()
|
|
await cursor.close()
|
|
if sub_row:
|
|
sub_dict = dict(sub_row)
|
|
for date_key in ['start_date', 'end_date']:
|
|
if sub_dict.get(date_key) and isinstance(
|
|
sub_dict[date_key], str):
|
|
try:
|
|
sub_dict[date_key] = datetime.fromisoformat(
|
|
sub_dict[date_key].replace("Z", "+00:00"))
|
|
except ValueError:
|
|
logging.warning(
|
|
f"Could not parse date string {sub_dict[date_key]} for key {date_key} in get_active_subscription"
|
|
)
|
|
return sub_dict
|
|
return None
|
|
finally:
|
|
await self._release_db(db, should_manage)
|
|
|
|
async def get_subscriptions_ending_soon(
|
|
self, days_threshold: int) -> List[Dict[str, Any]]:
|
|
db, should_manage = await self._get_db()
|
|
try:
|
|
now_utc = datetime.now(timezone.utc)
|
|
threshold_date = now_utc + timedelta(days=days_threshold)
|
|
today_date_str = now_utc.strftime('%Y-%m-%d')
|
|
query = """ SELECT s.user_id, u.first_name, u.language_code, s.end_date as end_date_raw, strftime('%Y-%m-%d', s.end_date) as end_date_str FROM subscriptions s JOIN users u ON s.user_id = u.user_id WHERE s.is_active = 1 AND s.end_date BETWEEN ? AND ? AND (s.last_notification_sent IS NULL OR s.last_notification_sent < ?) ORDER BY s.end_date ASC """
|
|
cursor = await db.execute(
|
|
query, (now_utc.isoformat(), threshold_date.isoformat(),
|
|
today_date_str))
|
|
rows = await cursor.fetchall()
|
|
await cursor.close()
|
|
processed_rows = []
|
|
for row_data in rows:
|
|
row_dict = dict(row_data)
|
|
end_date_obj_for_calc = None
|
|
if isinstance(row_dict['end_date_raw'], str):
|
|
try:
|
|
end_date_obj_for_calc = datetime.fromisoformat(
|
|
row_dict['end_date_raw'].replace("Z", "+00:00"))
|
|
if end_date_obj_for_calc.tzinfo is None:
|
|
end_date_obj_for_calc = end_date_obj_for_calc.replace(
|
|
tzinfo=timezone.utc)
|
|
row_dict['days_left'] = (end_date_obj_for_calc -
|
|
now_utc).total_seconds() / (
|
|
24 * 3600)
|
|
except ValueError:
|
|
row_dict['days_left'] = None
|
|
else:
|
|
row_dict['days_left'] = None
|
|
processed_rows.append(row_dict)
|
|
return processed_rows
|
|
finally:
|
|
await self._release_db(db, should_manage)
|
|
|
|
async def update_last_notification_sent(self, user_id: int,
|
|
subscription_end_date_iso: str):
|
|
db, should_manage = await self._get_db()
|
|
try:
|
|
today_iso_date_str = datetime.now(
|
|
timezone.utc).strftime('%Y-%m-%d')
|
|
await db.execute(
|
|
"UPDATE subscriptions SET last_notification_sent = ? WHERE user_id = ? AND is_active = 1 AND end_date = ?",
|
|
(today_iso_date_str, user_id, subscription_end_date_iso))
|
|
await db.commit()
|
|
except Exception as e:
|
|
logging.error(
|
|
f"Error updating last_notification_sent for {user_id} and end_date {subscription_end_date_iso}: {e}"
|
|
)
|
|
await db.rollback()
|
|
finally:
|
|
await self._release_db(db, should_manage)
|