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 )