""" Telegram Bot Handlers for ROA2WEB ERP Assistant This module implements all message and command handlers for the Telegram bot. Handles user interactions, authentication, and routes messages to Claude Agent. """ import logging from typing import Optional, Dict, Any from io import BytesIO from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup from telegram.ext import ContextTypes from telegram.constants import ParseMode from backend.modules.telegram.auth.linking import ( link_telegram_account, check_user_linked, get_user_auth_data, get_user_companies ) from backend.modules.telegram.agent.session import get_session_manager from backend.modules.telegram.db.operations import update_user_last_active, link_user_to_oracle from backend.modules.telegram.api.client import get_backend_client logger = logging.getLogger(__name__) # ============================================================================ # COMMAND HANDLERS # ============================================================================ async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Handle /start command. Handles two cases: 1. /start - Link Telegram account to Oracle account 2. /start - Show welcome message and instructions Args: update: Telegram update object context: Telegram context """ try: telegram_user = update.effective_user telegram_user_id = telegram_user.id args = context.args # Command arguments logger.info(f"/start command from user {telegram_user_id} (@{telegram_user.username})") # Case 1: /start - Link account if args and len(args) > 0: auth_code = args[0].upper() logger.info(f"Attempting to link user {telegram_user_id} with code {auth_code}") # ȘTERGE mesajul utilizatorului imediat (chat curat) try: await update.message.delete() except Exception as e: logger.warning(f"Could not delete /start message: {e}") # Check dacă user-ul a apăsat pe "Login din Web App" înainte web_login_msg_id = context.user_data.get('web_login_message_id') if web_login_msg_id: # EDITEAZĂ mesajul existent cu "Login din Web App" try: await context.bot.edit_message_text( chat_id=update.effective_chat.id, message_id=web_login_msg_id, text="Conectare cont...", parse_mode=ParseMode.MARKDOWN ) # IMPORTANT: NU ștergem message_id - îl păstrăm pentru editări ulterioare except Exception as e: logger.warning(f"Could not edit web_login message: {e}") # Fallback: creează mesaj nou dacă editarea a eșuat web_login_msg_id = None # Dacă nu avem mesaj de editat, creăm unul nou if not web_login_msg_id: # Creează mesaj nou și salvează ID-ul în context pentru editări ulterioare new_msg = await update.effective_chat.send_message( "Conectare cont...", parse_mode=ParseMode.MARKDOWN ) web_login_msg_id = new_msg.message_id # Salvează în context pentru consistență context.user_data['web_login_message_id'] = web_login_msg_id # Attempt linking result = await link_telegram_account(telegram_user, auth_code) if result: # Success! username = result['username'] jwt_token = result['jwt_token'] # Get session and company info session_manager = get_session_manager() session = await session_manager.get_or_create_session(telegram_user_id) company = session.get_active_company() company_name = company['name'] if company else None company_cui = company.get('cui') if company else None # Get cache status cache_enabled = None try: from backend.modules.telegram.api.client import get_backend_client client = get_backend_client() async with client: cache_stats = await client.get_cache_stats(jwt_token=jwt_token) cache_enabled = cache_stats.get('user_enabled', True) except Exception as e: logger.warning(f"Could not get cache status in /start: {e}") from backend.modules.telegram.bot.menus import create_main_menu, pad_message_for_wide_buttons keyboard = create_main_menu(company_name, company_cui, is_authenticated=True, cache_enabled=cache_enabled) # EDIT message to show menu with company if company_name: menu_text = f"{company_name}" else: menu_text = "Selectează o companie pentru a continua" menu_message = pad_message_for_wide_buttons(menu_text) # EDIT the login message (same message throughout the flow) await context.bot.edit_message_text( chat_id=update.effective_chat.id, message_id=web_login_msg_id, text=menu_message, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) logger.info(f"User {telegram_user_id} successfully linked to {username}") else: # Failed linking - EDIT message to show error await context.bot.edit_message_text( chat_id=update.effective_chat.id, message_id=web_login_msg_id, text="Cod invalid sau expirat\n\n" "Generează un cod nou din aplicația web și trimite:\n" "/start CODUL_TAU", parse_mode=ParseMode.MARKDOWN ) logger.warning(f"Failed to link user {telegram_user_id} with code {auth_code}") return # Case 2: /start (no args) - Show welcome/instructions is_linked = await check_user_linked(telegram_user_id) if is_linked: # FAZA 3: User is already linked - SHOW MENU auth_data = await get_user_auth_data(telegram_user_id) username = auth_data.get('username', 'utilizator') if auth_data else 'utilizator' # Get active company session_manager = get_session_manager() session = await session_manager.get_or_create_session(telegram_user_id) company = session.get_active_company() company_name = company['name'] if company else None company_cui = company.get('cui') if company else None # Create main menu from backend.modules.telegram.bot.menus import create_main_menu, get_menu_message keyboard = create_main_menu(company_name, company_cui, is_authenticated=True) menu_text = get_menu_message(company_name, company_cui) await update.message.reply_text( f"Bun venit, **{username}**\n\n{menu_text}", reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) else: # User not linked - show main menu with Login button from backend.modules.telegram.bot.menus import create_main_menu, pad_message_for_wide_buttons keyboard = create_main_menu( company_name=None, company_cui=None, is_authenticated=False, # Not authenticated - shows Login button cache_enabled=None ) welcome_text = pad_message_for_wide_buttons( "**Bun venit la ROA2WEB Bot**\n\n" "Pentru a incepe, te rog să te autentifici.\n\n" "Selectează o companie pentru a continua" ) await update.message.reply_text( welcome_text, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) except Exception as e: logger.error(f"Error in start_command: {e}", exc_info=True) await update.message.reply_text( "A aparut o eroare. Te rog incearca din nou mai tarziu." ) async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Handle /help command. Shows comprehensive help about bot capabilities and usage. Args: update: Telegram update object context: Telegram context """ try: telegram_user_id = update.effective_user.id logger.info(f"/help command from user {telegram_user_id}") help_text = """ **ROA2WEB Bot - Asistent Financiar** **Cum folosesc bot-ul?** Dupa conectarea contului, foloseste **butoanele interactive** pentru: **Operatiuni:** - Selectare companie activa - Vizualizare sold si situatie financiara - Trezorerie (Casa, Banca) - Sold Clienti cu detalii facturi - Sold Furnizori cu detalii facturi - Evolutie incasari/plati lunare **Navigare:** - Toate optiunile sunt accesibile prin butoane - Apasa pe numele companiei pentru a schimba compania activa - Foloseste butoanele "Refresh" pentru actualizare date - Foloseste "Meniu Principal" pentru a reveni la menu **Comenzi disponibile:** /start - Porneste bot-ul (cu/fara cod de linking) /menu - Afiseaza meniul principal cu butoane /help - Acest mesaj de ajutor /unlink - Deconecteaza contul (securitate) **Comenzi Cache (optimizare performanta):** /togglecache - Activeaza/Dezactiveaza cache pentru tine /clearcache - Sterge cache pentru compania activa /clearcache all - Sterge tot cache-ul **Conectare cont:** 1. Loghează-te în aplicația web ROA2WEB 2. Accesează Setări → Telegram Linking 3. Generează cod (valabil 15 minute) 4. Trimite codul în Telegram: `/start CODUL_TAU` **Securitate:** Datele sunt protejate prin autentificare JWT. Poti deconecta oricand cu /unlink. """ await update.message.reply_text(help_text, parse_mode=ParseMode.MARKDOWN) except Exception as e: logger.error(f"Error in help_command: {e}", exc_info=True) await update.message.reply_text( "A aparut o eroare. Te rog incearca din nou." ) async def clear_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Handle /clear command. Clears the active company selection for the user. Args: update: Telegram update object context: Telegram context """ try: telegram_user_id = update.effective_user.id logger.info(f"/clear command from user {telegram_user_id}") # Clear active company from session session_manager = get_session_manager() session = await session_manager.get_or_create_session(telegram_user_id) session.clear_active_company() await session_manager.save_session(telegram_user_id) await update.message.reply_text( "**Companie activa stearsa**\n\n" "Foloseste /selectcompany pentru a selecta alta companie.", parse_mode=ParseMode.MARKDOWN ) except Exception as e: logger.error(f"Error in clear_command: {e}", exc_info=True) await update.message.reply_text( "A apărut o eroare la ștergerea companiei active." ) async def clearcache_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Handle /clearcache command. Clears the cache for the current company or all companies. Usage: - /clearcache - Clear cache for current company - /clearcache all - Clear entire cache (all companies) Args: update: Telegram update object context: Telegram context """ try: telegram_user_id = update.effective_user.id logger.info(f"/clearcache command from user {telegram_user_id}") # Check if user is linked is_linked = await check_user_linked(telegram_user_id) if not is_linked: await update.message.reply_text( "**Cont neconectat**\n\nFoloseste /start pentru a conecta contul.", parse_mode=ParseMode.MARKDOWN ) return # Get auth data auth_data = await get_user_auth_data(telegram_user_id) jwt_token = auth_data['jwt_token'] # Check if user wants to clear all cache clear_all = len(context.args) > 0 and context.args[0].lower() == 'all' client = get_backend_client() async with client: if clear_all: # Clear entire cache result = await client.client.post( "/api/cache/invalidate", json={}, headers=client._get_auth_headers(jwt_token) ) if result.status_code == 200: await update.message.reply_text( "✅ **Cache șters complet**\n\n" "Toate datele cached au fost șterse.", parse_mode=ParseMode.MARKDOWN ) else: await update.message.reply_text("❌ Eroare la ștergerea cache-ului.") else: # Get active company session_manager = get_session_manager() from backend.modules.telegram.bot.helpers import get_active_company_or_prompt company = await get_active_company_or_prompt(update, session_manager, telegram_user_id) if not company: return # Clear cache for current company result = await client.client.post( "/api/cache/invalidate", json={"company_id": company['id']}, headers=client._get_auth_headers(jwt_token) ) if result.status_code == 200: await update.message.reply_text( f"✅ **Cache șters pentru {company['name']}**\n\n" "Datele vor fi reîncărcate la următoarea interogare.", parse_mode=ParseMode.MARKDOWN ) else: await update.message.reply_text("❌ Eroare la ștergerea cache-ului.") except Exception as e: logger.error(f"Error in clearcache_command: {e}", exc_info=True) await update.message.reply_text("Eroare la ștergerea cache-ului.") async def togglecache_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Handle /togglecache command. Toggles cache on/off for the current user. Args: update: Telegram update object context: Telegram context """ try: telegram_user_id = update.effective_user.id logger.info(f"/togglecache command from user {telegram_user_id}") # Check if user is linked is_linked = await check_user_linked(telegram_user_id) if not is_linked: await update.message.reply_text( "**Cont neconectat**\n\nFoloseste /start pentru a conecta contul.", parse_mode=ParseMode.MARKDOWN ) return # Get auth data auth_data = await get_user_auth_data(telegram_user_id) jwt_token = auth_data['jwt_token'] client = get_backend_client() async with client: # Get current cache stats to determine current state stats_response = await client.client.get( "/api/cache/stats", headers=client._get_auth_headers(jwt_token) ) if stats_response.status_code == 200: stats = stats_response.json() current_enabled = stats.get('user_cache_enabled', True) # Toggle to opposite state new_state = not current_enabled toggle_response = await client.client.post( "/api/cache/toggle-user", json={"enabled": new_state}, headers=client._get_auth_headers(jwt_token) ) if toggle_response.status_code == 200: if new_state: await update.message.reply_text( "✅ **Cache activat**\n\n" "Interogările tale vor folosi cache-ul pentru răspunsuri mai rapide.", parse_mode=ParseMode.MARKDOWN ) else: await update.message.reply_text( "⚠️ **Cache dezactivat**\n\n" "Interogările tale vor accesa direct baza de date Oracle.", parse_mode=ParseMode.MARKDOWN ) else: await update.message.reply_text("❌ Eroare la comutarea cache-ului.") else: await update.message.reply_text("❌ Eroare la citirea stării cache-ului.") except Exception as e: logger.error(f"Error in togglecache_command: {e}", exc_info=True) await update.message.reply_text("Eroare la comutarea cache-ului.") async def companies_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Handle /companies command. Shows list of companies the user has access to. Args: update: Telegram update object context: Telegram context """ try: telegram_user_id = update.effective_user.id logger.info(f"/companies command from user {telegram_user_id}") # Check if user is linked is_linked = await check_user_linked(telegram_user_id) if not is_linked: await update.message.reply_text( "**Cont neconectat**\n\n" "Conecteaza-ti contul cu /start", parse_mode=ParseMode.MARKDOWN ) return # Get companies companies = await get_user_companies(telegram_user_id) if not companies: await update.message.reply_text( "**Nicio companie gasita**\n\n" "Contacteaza administratorul pentru permisiuni.", parse_mode=ParseMode.MARKDOWN ) return # Format companies list companies_text = f"**Companiile tale ({len(companies)}):**\n\n" for i, comp in enumerate(companies, 1): nume = comp.get('nume_firma', 'N/A') comp_id = comp.get('id', 'N/A') cui = comp.get('cui', 'N/A') companies_text += f"{i}. **{nume}**\n" companies_text += f" - ID: {comp_id}\n" companies_text += f" - CUI: {cui}\n\n" companies_text += "\nFoloseste /selectcompany pentru a selecta compania activa." await update.message.reply_text(companies_text, parse_mode=ParseMode.MARKDOWN) except Exception as e: logger.error(f"Error in companies_command: {e}", exc_info=True) await update.message.reply_text( "A aparut o eroare la incarcarea companiilor." ) async def unlink_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Handle /unlink command. Unlinks the user's Telegram account from Oracle account (security feature). Args: update: Telegram update object context: Telegram context """ try: telegram_user_id = update.effective_user.id logger.info(f"/unlink command from user {telegram_user_id}") # Check if linked is_linked = await check_user_linked(telegram_user_id) if not is_linked: await update.message.reply_text( "Contul tau nu este linkuit.", parse_mode=ParseMode.MARKDOWN ) return # Create confirmation keyboard keyboard = [ [ InlineKeyboardButton("Da, deconecteaza", callback_data="unlink_confirm"), InlineKeyboardButton("Anuleaza", callback_data="unlink_cancel") ] ] reply_markup = InlineKeyboardMarkup(keyboard) await update.message.reply_text( "**Confirmare Deconectare**\n\n" "Esti sigur ca vrei sa deconectezi contul?\n\n" "Accesul la date va fi oprit. Poti reconecta oricand cu un cod nou.\n\n" "Confirma:", reply_markup=reply_markup, parse_mode=ParseMode.MARKDOWN ) except Exception as e: logger.error(f"Error in unlink_command: {e}", exc_info=True) await update.message.reply_text( "A aparut o eroare." ) async def selectcompany_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Handle /selectcompany [search_term] command. Permite căutare și selectare companie cu PAGINARE (identic cu butoanele). Args: update: Telegram update object context: Telegram context """ try: telegram_user_id = update.effective_user.id logger.info(f"/selectcompany command from user {telegram_user_id}") # Check if user is linked is_linked = await check_user_linked(telegram_user_id) if not is_linked: await update.message.reply_text( "**Cont neconectat**\n\nFoloseste /start", parse_mode=ParseMode.MARKDOWN ) return # Get auth data auth_data = await get_user_auth_data(telegram_user_id) jwt_token = auth_data['jwt_token'] # Get search term from command arguments (optional) search_term = " ".join(context.args) if context.args else "" # ✅ MODIFICARE: Folosim funcția comună cu paginare await _handle_selectcompany_view( query_or_update=update, telegram_user_id=telegram_user_id, jwt_token=jwt_token, is_callback=False, page=0, search_term=search_term ) except Exception as e: logger.error(f"Error in selectcompany_command: {e}", exc_info=True) await update.message.reply_text("A aparut o eroare. Te rog incearca din nou.") async def dashboard_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """Handle /dashboard command - shows financial dashboard.""" try: telegram_user_id = update.effective_user.id logger.info(f"/dashboard command from user {telegram_user_id}") # Check linked is_linked = await check_user_linked(telegram_user_id) if not is_linked: await update.message.reply_text( "**Cont neconectat**\n\nFoloseste /start", parse_mode=ParseMode.MARKDOWN ) return # Get active company session_manager = get_session_manager() from backend.modules.telegram.bot.helpers import get_active_company_or_prompt company = await get_active_company_or_prompt(update, session_manager, telegram_user_id) if not company: return # Prompt already sent # Get auth data auth_data = await get_user_auth_data(telegram_user_id) jwt_token = auth_data['jwt_token'] # ✅ MODIFICARE: Folosim funcția comună await _handle_sold_view( query_or_update=update, telegram_user_id=telegram_user_id, company=company, jwt_token=jwt_token, is_callback=False ) except Exception as e: logger.error(f"Error in dashboard_command: {e}", exc_info=True) await update.message.reply_text("Eroare la incarcarea dashboard-ului.") async def sold_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """Handle /sold command - alias for /dashboard.""" await dashboard_command(update, context) async def facturi_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """Handle /facturi [filtru] command - shows invoices list.""" try: telegram_user_id = update.effective_user.id logger.info(f"/facturi command from user {telegram_user_id}") # Check linked is_linked = await check_user_linked(telegram_user_id) if not is_linked: await update.message.reply_text( "**Cont nelinkuit**\n\nFoloseste /start.", parse_mode=ParseMode.MARKDOWN ) return # Get active company session_manager = get_session_manager() from backend.modules.telegram.bot.helpers import get_active_company_or_prompt company = await get_active_company_or_prompt(update, session_manager, telegram_user_id) if not company: return # Parse filters from args (optional: "neplatite", "platite", etc.) filters = {} if context.args: status_arg = context.args[0].lower() if status_arg in ['neplatite', 'unpaid']: filters['status'] = 'unpaid' elif status_arg in ['platite', 'paid']: filters['status'] = 'paid' # Get auth data auth_data = await get_user_auth_data(telegram_user_id) jwt_token = auth_data['jwt_token'] # Call API client = get_backend_client() async with client: invoices = await client.search_invoices( company_id=company['id'], jwt_token=jwt_token, filters=filters if filters else None ) # Format response from backend.modules.telegram.bot.formatters import format_invoices_response response = format_invoices_response(invoices, company['name']) # FAZA 3: Add action buttons from backend.modules.telegram.bot.menus import create_action_buttons keyboard = create_action_buttons("facturi", show_export=True) await update.message.reply_text( response, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) except Exception as e: logger.error(f"Error in facturi_command: {e}", exc_info=True) await update.message.reply_text("Eroare la incarcarea facturilor.") async def trezorerie_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Handle /trezorerie command - shows total treasury (casa + banca). Afișează sold total trezorerie cu defalcare și butoane pentru detalii. """ try: telegram_user_id = update.effective_user.id logger.info(f"/trezorerie command from user {telegram_user_id}") # Check linked is_linked = await check_user_linked(telegram_user_id) if not is_linked: await update.message.reply_text( "**Cont neconectat**\n\nFoloseste /start", parse_mode=ParseMode.MARKDOWN ) return # Get active company session_manager = get_session_manager() from backend.modules.telegram.bot.helpers import get_active_company_or_prompt company = await get_active_company_or_prompt(update, session_manager, telegram_user_id) if not company: return # Get auth data auth_data = await get_user_auth_data(telegram_user_id) jwt_token = auth_data['jwt_token'] # ✅ MODIFICARE: Folosim treasury_breakdown_split ca în Casa/Banca from backend.modules.telegram.bot.helpers import get_treasury_breakdown_split treasury_data = await get_treasury_breakdown_split( company_id=company['id'], jwt_token=jwt_token ) if not treasury_data: await update.message.reply_text("Eroare la incarcarea trezoreriei.") return # Extract cache metadata cache_hit = treasury_data.get('cache_hit', False) response_time_ms = treasury_data.get('response_time_ms', 0) cache_source = treasury_data.get('cache_source', None) # Format combined response (casa + banca) - rotunjit la leu (0 zecimale) casa_total = round(treasury_data['casa']['total']) banca_total = round(treasury_data['banca']['total']) total_treasury = casa_total + banca_total content = f"**Sold Total:** {total_treasury:,} RON\n\n" content += f"**Defalcare:**\n" content += f" - Casa: {casa_total:,} RON\n" content += f" - Banca: {banca_total:,} RON\n\n" content += "Foloseste butoanele pentru detalii:" # Apply company header formatting from backend.modules.telegram.bot.menus import format_response_with_company text = format_response_with_company(content, company['name']) # Add performance footer if response_time_ms > 0: from backend.modules.telegram.bot.formatters import add_performance_footer text = add_performance_footer(text, cache_hit, response_time_ms, cache_source) # Add buttons to view details from telegram import InlineKeyboardButton, InlineKeyboardMarkup keyboard = InlineKeyboardMarkup([ [ InlineKeyboardButton("Detalii Casa", callback_data="menu:casa"), InlineKeyboardButton("Detalii Banca", callback_data="menu:banca") ], [ InlineKeyboardButton("Meniu Principal", callback_data="action:menu") ] ]) await update.message.reply_text( text, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) except Exception as e: logger.error(f"Error in trezorerie_command: {e}", exc_info=True) await update.message.reply_text("Eroare la incarcarea trezoreriei.") async def menu_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Handle /menu command - shows main menu with interactive buttons. Displays the main menu with 6 financial options organized in a 2-column layout. Requires user to be linked and have an active company selected. Args: update: Telegram update object context: Telegram context """ try: telegram_user_id = update.effective_user.id logger.info(f"/menu command from user {telegram_user_id}") # Check if user is linked is_linked = await check_user_linked(telegram_user_id) if not is_linked: await update.message.reply_text( "**Cont neconectat**\n\nFoloseste /start", parse_mode=ParseMode.MARKDOWN ) return # Get active company session_manager = get_session_manager() session = await session_manager.get_or_create_session(telegram_user_id) company = session.get_active_company() # Get company data for menu company_name = company['name'] if company else None company_cui = company.get('cui') if company else None # Get cache status for user cache_enabled = None try: from backend.modules.telegram.api.client import get_backend_client client = get_backend_client() async with client: cache_stats = await client.get_cache_stats(jwt_token=auth_data['jwt_token']) cache_enabled = cache_stats.get('user_enabled', True) except Exception as e: logger.warning(f"Could not get cache status: {e}") # Create main menu (user is authenticated if they passed the is_linked check) from backend.modules.telegram.bot.menus import create_main_menu, get_menu_message keyboard = create_main_menu(company_name, company_cui, is_authenticated=True, cache_enabled=cache_enabled) menu_text = get_menu_message(company_name, company_cui) await update.message.reply_text( menu_text, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) except Exception as e: logger.error(f"Error in menu_command: {e}", exc_info=True) await update.message.reply_text("Eroare la afisarea meniului.") async def trezorerie_casa_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Handle /trezorerie_casa command - shows cash treasury data. Displays treasury data for cash accounts only (Casa). Args: update: Telegram update object context: Telegram context """ try: telegram_user_id = update.effective_user.id logger.info(f"/trezorerie_casa command from user {telegram_user_id}") # Check linked is_linked = await check_user_linked(telegram_user_id) if not is_linked: await update.message.reply_text( "**Cont neconectat**\n\nFoloseste /start", parse_mode=ParseMode.MARKDOWN ) return # Get active company session_manager = get_session_manager() from backend.modules.telegram.bot.helpers import get_active_company_or_prompt company = await get_active_company_or_prompt(update, session_manager, telegram_user_id) if not company: return # Prompt already sent # Get auth data auth_data = await get_user_auth_data(telegram_user_id) jwt_token = auth_data['jwt_token'] # Get treasury breakdown split from backend.modules.telegram.bot.helpers import get_treasury_breakdown_split treasury_data = await get_treasury_breakdown_split( company_id=company['id'], jwt_token=jwt_token ) if not treasury_data: await update.message.reply_text("Eroare la incarcarea trezoreriei cash.") return # Format response from backend.modules.telegram.bot.formatters import format_treasury_casa_response, add_performance_footer from backend.modules.telegram.bot.menus import create_action_buttons, format_response_with_company content = format_treasury_casa_response(treasury_data['casa']) response = format_response_with_company(content, company['name']) # Add performance footer if cache metadata is available if 'cache_hit' in treasury_data and 'response_time_ms' in treasury_data: cache_hit = treasury_data['cache_hit'] response_time_ms = treasury_data['response_time_ms'] cache_source = treasury_data.get('cache_source', None) response = add_performance_footer(response, cache_hit, response_time_ms, cache_source) keyboard = create_action_buttons("casa", show_export=True) await update.message.reply_text( response, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) except Exception as e: logger.error(f"Error in trezorerie_casa_command: {e}", exc_info=True) await update.message.reply_text("Eroare la incarcarea trezoreriei cash.") async def trezorerie_banca_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Handle /trezorerie_banca command - shows bank treasury data. Displays treasury data for bank accounts only (Banca). Args: update: Telegram update object context: Telegram context """ try: telegram_user_id = update.effective_user.id logger.info(f"/trezorerie_banca command from user {telegram_user_id}") # Check linked is_linked = await check_user_linked(telegram_user_id) if not is_linked: await update.message.reply_text( "**Cont neconectat**\n\nFoloseste /start", parse_mode=ParseMode.MARKDOWN ) return # Get active company session_manager = get_session_manager() from backend.modules.telegram.bot.helpers import get_active_company_or_prompt company = await get_active_company_or_prompt(update, session_manager, telegram_user_id) if not company: return # Prompt already sent # Get auth data auth_data = await get_user_auth_data(telegram_user_id) jwt_token = auth_data['jwt_token'] # Get treasury breakdown split from backend.modules.telegram.bot.helpers import get_treasury_breakdown_split treasury_data = await get_treasury_breakdown_split( company_id=company['id'], jwt_token=jwt_token ) if not treasury_data: await update.message.reply_text("Eroare la incarcarea trezoreriei bancare.") return # Format response from backend.modules.telegram.bot.formatters import format_treasury_banca_response, add_performance_footer from backend.modules.telegram.bot.menus import create_action_buttons, format_response_with_company content = format_treasury_banca_response(treasury_data['banca']) response = format_response_with_company(content, company['name']) # Add performance footer if cache metadata is available if 'cache_hit' in treasury_data and 'response_time_ms' in treasury_data: cache_hit = treasury_data['cache_hit'] response_time_ms = treasury_data['response_time_ms'] cache_source = treasury_data.get('cache_source', None) response = add_performance_footer(response, cache_hit, response_time_ms, cache_source) keyboard = create_action_buttons("banca", show_export=True) await update.message.reply_text( response, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) except Exception as e: logger.error(f"Error in trezorerie_banca_command: {e}", exc_info=True) await update.message.reply_text("Eroare la incarcarea trezoreriei bancare.") async def clienti_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Handle /clienti command - shows clients balance with maturity breakdown. Displays total clients balance, in-term and overdue amounts, and list of clients with interactive buttons to view details. Args: update: Telegram update object context: Telegram context """ try: telegram_user_id = update.effective_user.id logger.info(f"/clienti command from user {telegram_user_id}") # Check linked is_linked = await check_user_linked(telegram_user_id) if not is_linked: await update.message.reply_text( "**Cont neconectat**\n\nFoloseste /start", parse_mode=ParseMode.MARKDOWN ) return # Get active company session_manager = get_session_manager() from backend.modules.telegram.bot.helpers import get_active_company_or_prompt company = await get_active_company_or_prompt(update, session_manager, telegram_user_id) if not company: return # Prompt already sent # Get auth data auth_data = await get_user_auth_data(telegram_user_id) jwt_token = auth_data['jwt_token'] # Get clients with maturity data from backend.modules.telegram.bot.helpers import get_clients_with_maturity clients_data = await get_clients_with_maturity( company_id=company['id'], jwt_token=jwt_token ) if not clients_data: await update.message.reply_text("Eroare la incarcarea datelor clienti.") return # Extract cache metadata cache_hit = clients_data.get('cache_hit', False) response_time_ms = clients_data.get('response_time_ms', 0) cache_source = clients_data.get('cache_source', None) # Format response from backend.modules.telegram.bot.formatters import format_clients_balance_response, add_performance_footer from backend.modules.telegram.bot.menus import create_client_list_keyboard, format_response_with_company content = format_clients_balance_response( clients_data['clients'], clients_data['maturity'] ) response = format_response_with_company(content, company['name']) # Add performance footer if response_time_ms > 0: response = add_performance_footer(response, cache_hit, response_time_ms, cache_source) keyboard = create_client_list_keyboard(clients_data['clients'], page=0) await update.message.reply_text( response, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) except Exception as e: logger.error(f"Error in clienti_command: {e}", exc_info=True) await update.message.reply_text("Eroare la incarcarea datelor clienti.") async def furnizori_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Handle /furnizori command - shows suppliers balance with maturity breakdown. Displays total suppliers balance, in-term and overdue amounts, and list of suppliers with interactive buttons to view details. Args: update: Telegram update object context: Telegram context """ try: telegram_user_id = update.effective_user.id logger.info(f"/furnizori command from user {telegram_user_id}") # Check linked is_linked = await check_user_linked(telegram_user_id) if not is_linked: await update.message.reply_text( "**Cont neconectat**\n\nFoloseste /start", parse_mode=ParseMode.MARKDOWN ) return # Get active company session_manager = get_session_manager() from backend.modules.telegram.bot.helpers import get_active_company_or_prompt company = await get_active_company_or_prompt(update, session_manager, telegram_user_id) if not company: return # Prompt already sent # Get auth data auth_data = await get_user_auth_data(telegram_user_id) jwt_token = auth_data['jwt_token'] # Get suppliers with maturity data from backend.modules.telegram.bot.helpers import get_suppliers_with_maturity suppliers_data = await get_suppliers_with_maturity( company_id=company['id'], jwt_token=jwt_token ) if not suppliers_data: await update.message.reply_text("Eroare la incarcarea datelor furnizori.") return # Extract cache metadata cache_hit = suppliers_data.get('cache_hit', False) response_time_ms = suppliers_data.get('response_time_ms', 0) cache_source = suppliers_data.get('cache_source', None) # Format response from backend.modules.telegram.bot.formatters import format_suppliers_balance_response, add_performance_footer from backend.modules.telegram.bot.menus import create_supplier_list_keyboard, format_response_with_company content = format_suppliers_balance_response( suppliers_data['suppliers'], suppliers_data['maturity'] ) response = format_response_with_company(content, company['name']) # Add performance footer if response_time_ms > 0: response = add_performance_footer(response, cache_hit, response_time_ms, cache_source) keyboard = create_supplier_list_keyboard(suppliers_data['suppliers'], page=0) await update.message.reply_text( response, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) except Exception as e: logger.error(f"Error in furnizori_command: {e}", exc_info=True) await update.message.reply_text("Eroare la incarcarea datelor furnizori.") async def evolutie_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Handle /evolutie command - shows cash flow evolution (collections/payments). Displays performance data and monthly cash flow trends for collections and payments. Args: update: Telegram update object context: Telegram context """ try: telegram_user_id = update.effective_user.id logger.info(f"/evolutie command from user {telegram_user_id}") # Check linked is_linked = await check_user_linked(telegram_user_id) if not is_linked: await update.message.reply_text( "**Cont neconectat**\n\nFoloseste /start", parse_mode=ParseMode.MARKDOWN ) return # Get active company session_manager = get_session_manager() from backend.modules.telegram.bot.helpers import get_active_company_or_prompt company = await get_active_company_or_prompt(update, session_manager, telegram_user_id) if not company: return # Prompt already sent # Get auth data auth_data = await get_user_auth_data(telegram_user_id) jwt_token = auth_data['jwt_token'] # Get cash flow evolution data from backend.modules.telegram.bot.helpers import get_cashflow_evolution_data evolution_data = await get_cashflow_evolution_data( company_id=company['id'], jwt_token=jwt_token ) if not evolution_data: await update.message.reply_text("Eroare la incarcarea datelor evolutie.") return # Format response from backend.modules.telegram.bot.formatters import format_cashflow_evolution_response, add_performance_footer from backend.modules.telegram.bot.menus import create_action_buttons, format_response_with_company content = format_cashflow_evolution_response( evolution_data['performance'], evolution_data['monthly'] ) response = format_response_with_company(content, company['name']) # Add performance footer if cache metadata is available if 'cache_hit' in evolution_data and 'response_time_ms' in evolution_data: cache_hit = evolution_data['cache_hit'] response_time_ms = evolution_data['response_time_ms'] cache_source = evolution_data.get('cache_source', None) response = add_performance_footer(response, cache_hit, response_time_ms, cache_source) keyboard = create_action_buttons("evolutie", show_export=False, show_refresh=False) await update.message.reply_text( response, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) except Exception as e: logger.error(f"Error in evolutie_command: {e}", exc_info=True) await update.message.reply_text("Eroare la incarcarea datelor evolutie.") # ============================================================================ # TEXT MESSAGE HANDLERS # ============================================================================ async def handle_text_message(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Handle regular text messages. Automatically detects and processes linking codes when user sends a text that matches the code format (8 alphanumeric characters). Args: update: Telegram update object context: Telegram context """ try: telegram_user = update.effective_user telegram_user_id = telegram_user.id text = update.message.text.strip() logger.info(f"Text message from user {telegram_user_id}") # Check if user is awaiting password for server switch pending_server_id = context.user_data.get('pending_switch_server_id') if pending_server_id: # Șterge IMEDIAT mesajul cu parola (securitate) try: await update.message.delete() except Exception as e: logger.warning(f"Could not delete password message: {e}") oracle_password = text jwt_token = context.user_data.pop('pending_switch_jwt_token', None) username = context.user_data.pop('pending_switch_username', None) context.user_data.pop('pending_switch_server_id', None) if not jwt_token or not username: await update.effective_chat.send_message("Sesiune expirată. Încearcă din nou.") return await update.effective_chat.send_message("Se verifică parola și se schimbă serverul...") client = get_backend_client() async with client: result = await client.switch_server( jwt_token=jwt_token, oracle_username=username, new_server_id=pending_server_id, oracle_password=oracle_password ) if not result.get('success'): from telegram import InlineKeyboardButton, InlineKeyboardMarkup await update.effective_chat.send_message( f"❌ {result.get('message', 'Eroare la schimbarea serverului')}\n\nReîncearcă cu /menu → Schimbă server.", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("« Meniu", callback_data="action:menu")]]) ) return # Salvează noul JWT în SQLite from datetime import datetime, timedelta token_expires_at = datetime.now() + timedelta(minutes=30) await link_user_to_oracle( telegram_user_id=telegram_user_id, oracle_username=result.get('username', username), jwt_token=result['access_token'], jwt_refresh_token=result['refresh_token'], token_expires_at=token_expires_at ) # Curăță compania din sesiune — aparținea serverului vechi session_manager = get_session_manager() session = await session_manager.get_or_create_session(telegram_user_id) session.clear_active_company() await session_manager.save_session(telegram_user_id) try: from backend.config import settings srv = settings.get_oracle_server(pending_server_id) srv_display = srv.name if srv else pending_server_id except Exception: srv_display = pending_server_id await update.effective_chat.send_message(f"✅ Server schimbat: **{srv_display}**\nSelectează firma...", parse_mode=ParseMode.MARKDOWN) await _handle_selectcompany_view( query_or_update=update, telegram_user_id=telegram_user_id, jwt_token=result['access_token'], is_callback=False, page=0, search_term="" ) return # Check if user is already linked is_linked = await check_user_linked(telegram_user_id) if is_linked: # User is already linked - ignore text messages # (could add natural language processing here in the future) return text = text.upper() # Only uppercase for linking code check # User is NOT linked - check if text looks like a linking code # Linking codes are exactly 8 alphanumeric characters if len(text) == 8 and text.isalnum(): logger.info(f"Detected potential linking code: {text} from user {telegram_user_id}") # ȘTERGE mesajul utilizatorului cu codul (chat curat) try: await update.message.delete() except Exception as e: logger.warning(f"Could not delete code message: {e}") # Check dacă există mesaj de editat din "Login din Web App" web_login_msg_id = context.user_data.get('web_login_message_id') if web_login_msg_id: # EDITEAZĂ mesajul existent cu instrucțiunile try: await context.bot.edit_message_text( chat_id=update.effective_chat.id, message_id=web_login_msg_id, text="Conectare cont...", parse_mode=ParseMode.MARKDOWN ) except Exception as e: logger.warning(f"Could not edit web_login message: {e}") # Fallback: creează mesaj nou web_login_msg_id = None # Dacă nu există mesaj de editat, creează unul nou if not web_login_msg_id: linking_msg = await update.effective_chat.send_message( "Conectare cont...", parse_mode=ParseMode.MARKDOWN ) web_login_msg_id = linking_msg.message_id # Attempt linking result = await link_telegram_account(telegram_user, text) if result: # Success! username = result['username'] # Show main menu with buttons for newly linked user session_manager = get_session_manager() session = await session_manager.get_or_create_session(telegram_user_id) company = session.get_active_company() company_name = company['name'] if company else None company_cui = company.get('cui') if company else None from backend.modules.telegram.bot.menus import create_main_menu, pad_message_for_wide_buttons keyboard = create_main_menu(company_name, company_cui, is_authenticated=True) # Create menu text if company_name: menu_text = f"{company_name}" else: menu_text = "Selectează o companie pentru a continua" menu_message = pad_message_for_wide_buttons(menu_text) # EDIT the same message to show menu (no new messages!) await context.bot.edit_message_text( chat_id=update.effective_chat.id, message_id=web_login_msg_id, text=menu_message, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) logger.info(f"User {telegram_user_id} successfully linked to {username} via direct code input") else: # Failed linking - EDIT the same message to show error await context.bot.edit_message_text( chat_id=update.effective_chat.id, message_id=web_login_msg_id, text="Cod invalid sau expirat\n\n" "Generează un cod nou din aplicația web și trimite-l direct.\n\n" "Codul expiră în 15 minute.", parse_mode=ParseMode.MARKDOWN ) logger.warning(f"Failed to link user {telegram_user_id} with direct code: {text}") else: # Text doesn't look like a linking code # Show helpful message keyboard = InlineKeyboardMarkup([ [InlineKeyboardButton("Cum obtin codul de link?", callback_data="login_help")], [InlineKeyboardButton("Am deja cod - Linkez contul", callback_data="login_prompt")] ]) await update.message.reply_text( "**Salut**\n\n" "Pentru a folosi bot-ul, conecteaza contul tau ROA2WEB.\n\n" "Codul are exact 8 caractere (exemplu: ABC12XYZ)\n\n" "Alege o optiune:", reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) except Exception as e: logger.error(f"Error in handle_text_message: {e}", exc_info=True) await update.message.reply_text( "A aparut o eroare. Te rog incearca din nou." ) # ============================================================================ # CALLBACK QUERY HANDLERS (for inline buttons) # ============================================================================ async def handle_menu_callback(query, telegram_user_id: int, callback_data: str): """ Handle main menu button clicks. Callback format: menu:{action} Actions: sold, casa, banca, clienti, furnizori, evolutie, select_company Args: query: CallbackQuery object telegram_user_id: Telegram user ID callback_data: Callback data string """ action = callback_data.split(":")[1] # Get auth data auth_data = await get_user_auth_data(telegram_user_id) # If user is not authenticated and trying to access financial data, show auth required message if auth_data is None and action != "select_company": from backend.modules.telegram.bot.menus import create_main_menu, pad_message_for_wide_buttons keyboard = create_main_menu(company_name=None, company_cui=None, is_authenticated=False) menu_text = pad_message_for_wide_buttons( "⚠️ **Autentificare necesară**\n\n" "Pentru a accesa date financiare,\n" "apasă **Login** și urmează instrucțiunile." ) await query.edit_message_text( menu_text, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) return # If action is select_company and user is not authenticated, allow it (will show empty list or error) if action == "select_company" and auth_data is None: await query.answer("Pentru a vedea companiile, trebuie să te autentifici mai întâi.", show_alert=True) return jwt_token = auth_data['jwt_token'] # Get active company session_manager = get_session_manager() session = await session_manager.get_or_create_session(telegram_user_id) company = session.get_active_company() if not company and action != "select_company": # Get companies and show selection directly client = get_backend_client() async with client: companies = await client.get_user_companies(jwt_token=jwt_token) if not companies: await query.edit_message_text( "Nu ai acces la nicio companie.\n" "Contacteaza administratorul.", parse_mode=ParseMode.MARKDOWN ) return from backend.modules.telegram.bot.helpers import create_company_selection_keyboard_paginated keyboard = create_company_selection_keyboard_paginated(companies, page=0) await query.edit_message_text( f"**Selecteaza mai intai o companie**\n\n" f"Companiile tale ({len(companies)}):", reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) return # Route to appropriate handler if action == "sold": # ✅ MODIFICARE: Folosim funcția comună await _handle_sold_view( query_or_update=query, telegram_user_id=telegram_user_id, company=company, jwt_token=jwt_token, is_callback=True ) elif action == "trezorerie": # Trezorerie unified (Casa + Banca combined) from backend.modules.telegram.bot.helpers import get_treasury_breakdown_split treasury_data = await get_treasury_breakdown_split(company['id'], jwt_token) from backend.modules.telegram.bot.formatters import format_treasury_combined_response, add_performance_footer from backend.modules.telegram.bot.menus import create_action_buttons, format_response_with_company content = format_treasury_combined_response(treasury_data) response = format_response_with_company(content, company['name']) # Add performance footer if cache metadata is available if 'cache_hit' in treasury_data and 'response_time_ms' in treasury_data: cache_hit = treasury_data['cache_hit'] response_time_ms = treasury_data['response_time_ms'] cache_source = treasury_data.get('cache_source', None) response = add_performance_footer(response, cache_hit, response_time_ms, cache_source) keyboard = create_action_buttons("trezorerie", show_export=False, show_refresh=False) try: await query.edit_message_text( response, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) except Exception as e: # Ignore "Message is not modified" error if "Message is not modified" not in str(e): raise elif action == "casa": # Trezorerie casa from backend.modules.telegram.bot.helpers import get_treasury_breakdown_split treasury_data = await get_treasury_breakdown_split(company['id'], jwt_token) from backend.modules.telegram.bot.formatters import format_treasury_casa_response, add_performance_footer from backend.modules.telegram.bot.menus import create_action_buttons, format_response_with_company content = format_treasury_casa_response(treasury_data['casa']) response = format_response_with_company(content, company['name']) # Add performance footer if cache metadata is available if 'cache_hit' in treasury_data and 'response_time_ms' in treasury_data: cache_hit = treasury_data['cache_hit'] response_time_ms = treasury_data['response_time_ms'] cache_source = treasury_data.get('cache_source', None) response = add_performance_footer(response, cache_hit, response_time_ms, cache_source) keyboard = create_action_buttons("casa", show_export=False, show_refresh=False) try: await query.edit_message_text( response, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) except Exception as e: # Ignore "Message is not modified" error if "Message is not modified" not in str(e): raise elif action == "banca": # Trezorerie banca from backend.modules.telegram.bot.helpers import get_treasury_breakdown_split treasury_data = await get_treasury_breakdown_split(company['id'], jwt_token) from backend.modules.telegram.bot.formatters import format_treasury_banca_response, add_performance_footer from backend.modules.telegram.bot.menus import create_action_buttons, format_response_with_company content = format_treasury_banca_response(treasury_data['banca']) response = format_response_with_company(content, company['name']) # Add performance footer if cache metadata is available if 'cache_hit' in treasury_data and 'response_time_ms' in treasury_data: cache_hit = treasury_data['cache_hit'] response_time_ms = treasury_data['response_time_ms'] cache_source = treasury_data.get('cache_source', None) response = add_performance_footer(response, cache_hit, response_time_ms, cache_source) keyboard = create_action_buttons("banca", show_export=False, show_refresh=False) try: await query.edit_message_text( response, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) except Exception as e: # Ignore "Message is not modified" error if "Message is not modified" not in str(e): raise elif action == "clienti": # Sold clienți + listă cu paginare from backend.modules.telegram.bot.helpers import get_clients_with_maturity clients_data = await get_clients_with_maturity(company['id'], jwt_token) from backend.modules.telegram.bot.formatters import format_clients_balance_response, add_performance_footer from backend.modules.telegram.bot.menus import create_client_list_keyboard, format_response_with_company content = format_clients_balance_response( clients_data['clients'], clients_data['maturity'] ) response = format_response_with_company(content, company['name']) # Add performance footer if cache metadata is available if 'cache_hit' in clients_data and 'response_time_ms' in clients_data: cache_hit = clients_data['cache_hit'] response_time_ms = clients_data['response_time_ms'] cache_source = clients_data.get('cache_source', None) response = add_performance_footer(response, cache_hit, response_time_ms, cache_source) keyboard = create_client_list_keyboard(clients_data['clients'], page=0) await query.edit_message_text( response, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) elif action == "furnizori": # Sold furnizori + listă cu paginare from backend.modules.telegram.bot.helpers import get_suppliers_with_maturity suppliers_data = await get_suppliers_with_maturity(company['id'], jwt_token) from backend.modules.telegram.bot.formatters import format_suppliers_balance_response, add_performance_footer from backend.modules.telegram.bot.menus import create_supplier_list_keyboard, format_response_with_company content = format_suppliers_balance_response( suppliers_data['suppliers'], suppliers_data['maturity'] ) response = format_response_with_company(content, company['name']) # Add performance footer if cache metadata is available if 'cache_hit' in suppliers_data and 'response_time_ms' in suppliers_data: cache_hit = suppliers_data['cache_hit'] response_time_ms = suppliers_data['response_time_ms'] cache_source = suppliers_data.get('cache_source', None) response = add_performance_footer(response, cache_hit, response_time_ms, cache_source) keyboard = create_supplier_list_keyboard(suppliers_data['suppliers'], page=0) await query.edit_message_text( response, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) elif action == "evolutie": # Evoluție cash flow from backend.modules.telegram.bot.helpers import get_cashflow_evolution_data evolution_data = await get_cashflow_evolution_data(company['id'], jwt_token) from backend.modules.telegram.bot.formatters import format_cashflow_evolution_response, add_performance_footer from backend.modules.telegram.bot.menus import create_action_buttons, format_response_with_company content = format_cashflow_evolution_response( evolution_data['performance'], evolution_data['monthly'] ) response = format_response_with_company(content, company['name']) # Add performance footer if cache metadata is available if 'cache_hit' in evolution_data and 'response_time_ms' in evolution_data: cache_hit = evolution_data['cache_hit'] response_time_ms = evolution_data['response_time_ms'] cache_source = evolution_data.get('cache_source', None) response = add_performance_footer(response, cache_hit, response_time_ms, cache_source) keyboard = create_action_buttons("evolutie", show_export=False, show_refresh=False) await query.edit_message_text( response, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) elif action == "togglecache": # Toggle cache pentru user try: client = get_backend_client() async with client: cache_stats = await client.get_cache_stats(jwt_token=jwt_token) user_enabled = cache_stats.get('user_enabled', True) # Create toggle buttons from telegram import InlineKeyboardButton, InlineKeyboardMarkup keyboard = [ [ InlineKeyboardButton( "✅ Activează" if not user_enabled else "❌ Dezactivează", callback_data=f"cache_toggle:{'on' if not user_enabled else 'off'}" ) ], [InlineKeyboardButton("« Înapoi la Meniu", callback_data="action:menu")] ] reply_markup = InlineKeyboardMarkup(keyboard) status = "ACTIVAT" if user_enabled else "DEZACTIVAT" message = f"**Cache Status**\n\nCurent: {status}\n\n" if user_enabled: message += "Vrei să dezactivezi cache-ul temporar?\nFolosește pentru teste de performanță." else: message += "Cache-ul este dezactivat.\nToate queries merg direct la Oracle." await query.edit_message_text( message, reply_markup=reply_markup, parse_mode=ParseMode.MARKDOWN ) except Exception as e: logger.error(f"Toggle cache menu error: {e}", exc_info=True) await query.answer("Eroare la obținerea status cache.", show_alert=True) elif action == "clearcache": # Clear cache try: # Create inline keyboard from telegram import InlineKeyboardButton, InlineKeyboardMarkup keyboard = [ [ InlineKeyboardButton("Toate companiile", callback_data="cache_clear:all"), InlineKeyboardButton("Doar compania mea", callback_data="cache_clear:current") ], [InlineKeyboardButton("« Înapoi la Meniu", callback_data="action:menu")] ] reply_markup = InlineKeyboardMarkup(keyboard) message = "**🔄 Invalidare Cache**\n\n" if company: message += f"Compania curentă: {company['name']}\n\n" message += "Alege scope:" await query.edit_message_text( message, reply_markup=reply_markup, parse_mode=ParseMode.MARKDOWN ) except Exception as e: logger.error(f"Clear cache menu error: {e}", exc_info=True) await query.answer("Eroare la afișarea opțiuni cache.", show_alert=True) elif action == "select_company": # ✅ MODIFICARE: Folosim funcția comună await _handle_selectcompany_view( query_or_update=query, telegram_user_id=telegram_user_id, jwt_token=jwt_token, is_callback=True, page=0, search_term="" ) elif action == "switch_server": from telegram import InlineKeyboardButton, InlineKeyboardMarkup from shared.auth.email_server_cache import email_server_cache from backend.modules.telegram.bot.menus import pad_message_for_wide_buttons username = auth_data['username'] try: servers = await email_server_cache.get_servers_for_username(username) except Exception as e: logger.error(f"Could not get servers for {username}: {e}") await query.answer("Eroare la obținerea serverelor.", show_alert=True) return if len(servers) <= 1: await query.answer("Ești pe singurul server disponibil.", show_alert=True) return # Build server selection keyboard try: from backend.config import settings keyboard_rows = [] for srv_id in servers: srv = settings.get_oracle_server(srv_id) srv_name = srv.name if srv else srv_id keyboard_rows.append([InlineKeyboardButton(srv_name, callback_data=f"switch_server_confirm:{srv_id}")]) except Exception: keyboard_rows = [[InlineKeyboardButton(s, callback_data=f"switch_server_confirm:{s}")] for s in servers] keyboard_rows.append([InlineKeyboardButton("« Înapoi", callback_data="action:menu")]) await query.edit_message_text( pad_message_for_wide_buttons(f"Selectează serverul Oracle:\n\nUtilizator: {username}"), reply_markup=InlineKeyboardMarkup(keyboard_rows), parse_mode=ParseMode.MARKDOWN ) async def handle_action_callback(query, telegram_user_id: int, callback_data: str): """ Handle action button clicks (Refresh, Export, Menu, Help, Login, Logout). Callback format: action:{type}:{view} Types: refresh, export, menu, help, login, logout Args: query: CallbackQuery object telegram_user_id: Telegram user ID callback_data: Callback data string """ parts = callback_data.split(":") action_type = parts[1] if action_type == "menu": # Back to main menu session_manager = get_session_manager() session = await session_manager.get_or_create_session(telegram_user_id) company = session.get_active_company() # Check if user is authenticated auth_data = await get_user_auth_data(telegram_user_id) is_authenticated = auth_data is not None # Get cache status for user cache_enabled = None if is_authenticated: try: from backend.modules.telegram.api.client import get_backend_client client = get_backend_client() async with client: cache_stats = await client.get_cache_stats(jwt_token=auth_data['jwt_token']) cache_enabled = cache_stats.get('user_enabled', True) except Exception as e: logger.warning(f"Could not get cache status: {e}") from backend.modules.telegram.bot.menus import create_main_menu, get_menu_message company_name = company['name'] if company else None company_cui = company.get('cui') if company else None keyboard = create_main_menu(company_name, company_cui, is_authenticated, cache_enabled) menu_text = get_menu_message(company_name, company_cui) await query.edit_message_text( menu_text, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) elif action_type == "refresh": # Refresh current view view = parts[2] if len(parts) > 2 else "sold" # Check if it's a detail view (client_detail:name or supplier_detail:name) if view.startswith("client_detail:"): entity_name = view.split(":", 1)[1] # Extract entity name # Limit name to 40 chars for Telegram callback_data limit (64 bytes) safe_name = entity_name[:40] if len(entity_name) > 40 else entity_name await handle_details_callback(query, telegram_user_id, f"details:client:{safe_name}:0") elif view.startswith("supplier_detail:"): entity_name = view.split(":", 1)[1] # Extract entity name # Limit name to 40 chars for Telegram callback_data limit (64 bytes) safe_name = entity_name[:40] if len(entity_name) > 40 else entity_name await handle_details_callback(query, telegram_user_id, f"details:supplier:{safe_name}:0") else: # Regular menu view refresh await handle_menu_callback(query, telegram_user_id, f"menu:{view}") elif action_type == "export": # Export functionality (placeholder for now) await query.answer("Functia de export va fi disponibila in curand", show_alert=True) elif action_type == "help": # Show help message above menu (edit current message) from backend.modules.telegram.bot.menus import pad_message_for_wide_buttons, create_main_menu # Get auth status and company info session_manager = get_session_manager() session = await session_manager.get_or_create_session(telegram_user_id) company = session.get_active_company() auth_data = await get_user_auth_data(telegram_user_id) is_authenticated = auth_data is not None company_name = company['name'] if company else None company_cui = company.get('cui') if company else None keyboard = create_main_menu(company_name, company_cui, is_authenticated) help_text = pad_message_for_wide_buttons( "**Ghid Rapid**\n\n" "**Selectare Companie** - Alege compania activă\n\n" "**Sold Companie** - Dashboard financiar complet\n" "**Trezorerie Casa** - Situație conturi cash\n" "**Trezorerie Banca** - Situație conturi bancare\n" "**Sold Clienti** - Clienți + facturi neplătite\n" "**Sold Furnizori** - Furnizori + facturi\n" "**Evolutie Incasari** - Trend lunar încasări\n\n" "**Logout** - Deconectează contul\n\n" "_Toate datele sunt în timp real din Oracle._" ) await query.edit_message_text( help_text, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) elif action_type == "logout": # Show logout confirmation from backend.modules.telegram.bot.menus import pad_message_for_wide_buttons confirmation_text = pad_message_for_wide_buttons( "**Confirmare Deconectare**\n\n" "Ești sigur că vrei să deconectezi contul?\n\n" "Accesul la date va fi oprit.\n" "Poți reconecta oricând cu un cod nou." ) keyboard = InlineKeyboardMarkup([ [ InlineKeyboardButton("Da, deconectează", callback_data="logout_confirm"), InlineKeyboardButton("Anulează", callback_data="logout_cancel") ] ]) await query.edit_message_text( confirmation_text, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) # NOTE: action:login is handled by email_login_handler ConversationHandler # See app/bot/email_handlers.py for the complete email authentication flow async def handle_details_callback(query, telegram_user_id: int, callback_data: str): """ Handle client/supplier detail clicks. Callback format: details:{type}:{name}:{page} Types: client, supplier Args: query: CallbackQuery object telegram_user_id: Telegram user ID callback_data: Callback data string """ parts = callback_data.split(":") detail_type = parts[1] # client or supplier entity_name = parts[2] # client/supplier name page = int(parts[3]) if len(parts) > 3 else 0 # invoice page number # Get auth data and company auth_data = await get_user_auth_data(telegram_user_id) jwt_token = auth_data['jwt_token'] session_manager = get_session_manager() session = await session_manager.get_or_create_session(telegram_user_id) company = session.get_active_company() if detail_type == "client": # Get client details (from clients list) # entity_name might be truncated to 40 chars, so search by startswith from backend.modules.telegram.bot.helpers import get_clients_with_maturity clients_data = await get_clients_with_maturity(company['id'], jwt_token) # Find client by full or partial name match client = next((c for c in clients_data['clients'] if c['name'].startswith(entity_name)), None) if not client: await query.answer("Client negăsit", show_alert=True) return # Use FULL client name for invoice search (not truncated) full_client_name = client['name'] # Get client invoices by FULL name from backend.modules.telegram.bot.helpers import get_client_invoices invoices = await get_client_invoices(company['id'], full_client_name, jwt_token) # Format response from backend.modules.telegram.bot.formatters import format_client_detail_response from backend.modules.telegram.bot.menus import create_action_buttons, format_response_with_company content = format_client_detail_response(client, invoices) response = format_response_with_company(content, company['name']) # Use truncated name for callback_data (to stay within 64 byte limit) safe_name = entity_name[:40] if len(entity_name) > 40 else entity_name keyboard = create_action_buttons(f"client_detail:{safe_name}", show_export=False, show_back=True, show_refresh=False) await query.edit_message_text( response, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) elif detail_type == "supplier": # Get supplier details (from suppliers list) # entity_name might be truncated to 40 chars, so search by startswith from backend.modules.telegram.bot.helpers import get_suppliers_with_maturity suppliers_data = await get_suppliers_with_maturity(company['id'], jwt_token) # Find supplier by full or partial name match supplier = next((s for s in suppliers_data['suppliers'] if s['name'].startswith(entity_name)), None) if not supplier: await query.answer("Furnizor negăsit", show_alert=True) return # Use FULL supplier name for invoice search (not truncated) full_supplier_name = supplier['name'] # Get supplier invoices by FULL name from backend.modules.telegram.bot.helpers import get_supplier_invoices invoices = await get_supplier_invoices(company['id'], full_supplier_name, jwt_token) # Format response from backend.modules.telegram.bot.formatters import format_supplier_detail_response from backend.modules.telegram.bot.menus import create_action_buttons, format_response_with_company content = format_supplier_detail_response(supplier, invoices) response = format_response_with_company(content, company['name']) # Use truncated name for callback_data (to stay within 64 byte limit) safe_name = entity_name[:40] if len(entity_name) > 40 else entity_name keyboard = create_action_buttons(f"supplier_detail:{safe_name}", show_export=False, show_back=True, show_refresh=False) await query.edit_message_text( response, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) async def handle_invoice_callback(query, telegram_user_id: int, callback_data: str): """ Handle invoice detail clicks. Callback format: invoice:{partner_type}:{id} Args: query: CallbackQuery object telegram_user_id: Telegram user ID callback_data: Callback data string """ parts = callback_data.split(":") partner_type = parts[1] # CLIENTI or FURNIZORI invoice_id = int(parts[2]) # Get invoice details from API (placeholder for now) await query.answer("Detalii factura (in dezvoltare)", show_alert=True) async def handle_navigation_back(query, telegram_user_id: int, callback_data: str): """ Handle back navigation. Callback format: nav:back:{location} Locations: menu, clienti, furnizori Args: query: CallbackQuery object telegram_user_id: Telegram user ID callback_data: Callback data string """ location = callback_data.split(":")[2] if location == "menu": # Back to main menu await handle_action_callback(query, telegram_user_id, "action:menu") elif location == "clienti": # Back to clients list await handle_menu_callback(query, telegram_user_id, "menu:clienti") elif location == "furnizori": # Back to suppliers list await handle_menu_callback(query, telegram_user_id, "menu:furnizori") async def button_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Handle inline button callbacks. Callback data formats: - login_help - Show help on how to get link code - login_prompt - Prompt user to enter link code - login_back - Back to welcome message - menu:{action} - Main menu buttons - action:{type}:{view} - Action buttons (refresh, export, menu) - details:{type}:{id} - Client/Supplier details - invoice:{partner_type}:{id} - Invoice details - nav:back:{location} - Navigation back - select_company:{id} - Company selection (existing) - unlink_confirm/unlink_cancel - Unlink confirmation (existing) Args: update: Telegram update object context: Telegram context """ try: query = update.callback_query callback_data = query.data # ========== IGNORE CALLBACKS HANDLED BY CONVERSATION HANDLER ========== # These callbacks are managed by email_login_handler ConversationHandler # Return immediately without answering to let ConversationHandler process them conversation_patterns = [ 'action:login', # Login button from menu 'email_login', # Email login button 'web_login_info', # Web app login info button 'cancel', # Cancel button ] # Check exact matches if callback_data in conversation_patterns: logger.info(f"[BUTTON_CALLBACK] Ignoring {callback_data} - handled by ConversationHandler") return # Check prefix matches (e.g., resend:email@example.com) if callback_data.startswith('resend:'): logger.info(f"[BUTTON_CALLBACK] Ignoring {callback_data} - handled by ConversationHandler") return # ========== PROCESS ALL OTHER CALLBACKS ========== await query.answer() telegram_user_id = update.effective_user.id logger.info(f"Button callback: {callback_data} from user {telegram_user_id}") # ========== EXISTING CALLBACKS (preserve) ========== # Handle pagination for company selection if callback_data.startswith("select_company_page:"): # Extract page number page = int(callback_data.split(":")[1]) # Get companies auth_data = await get_user_auth_data(telegram_user_id) jwt_token = auth_data['jwt_token'] client = get_backend_client() async with client: companies = await client.get_user_companies(jwt_token=jwt_token) # Create paginated keyboard for requested page from backend.modules.telegram.bot.helpers import create_company_selection_keyboard_paginated keyboard = create_company_selection_keyboard_paginated(companies, page=page) await query.edit_message_text( f"**Selecteaza Compania**\n\n" f"Companiile tale ({len(companies)}):", reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) elif callback_data == "select_company_alpha_menu": # Show A-Z letter filter keyboard from backend.modules.telegram.bot.helpers import create_alpha_filter_keyboard await query.edit_message_text( "**Selectează litera**\n\nAlege prima literă a firmei:", reply_markup=create_alpha_filter_keyboard(), parse_mode=ParseMode.MARKDOWN ) elif callback_data.startswith("select_company_alpha:"): # Filter companies by starting letter and show page 0 letter = callback_data.split(":", 1)[1] # "A" or "ALL" auth_data = await get_user_auth_data(telegram_user_id) jwt_token = auth_data['jwt_token'] client = get_backend_client() async with client: all_companies = await client.get_user_companies(jwt_token=jwt_token) if letter == "ALL": filtered = all_companies else: filtered = [ c for c in all_companies if c.get('name', c.get('nume_firma', '')).upper().startswith(letter) ] if not filtered: await query.answer(f"Nicio firmă cu litera {letter}.", show_alert=True) return from backend.modules.telegram.bot.helpers import create_company_selection_keyboard_paginated keyboard = create_company_selection_keyboard_paginated( filtered, page=0, back_callback="select_company_alpha_menu", page_callback_prefix="select_company_alpha_page", page_callback_suffix=f":{letter}" ) label = f"Firme cu litera {letter}" if letter != "ALL" else "Toate firmele" await query.edit_message_text( f"**{label}** ({len(filtered)}):", reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) elif callback_data.startswith("select_company_alpha_page:"): # Paginate within an alpha-filtered company list # Callback format: select_company_alpha_page:PAGE:LETTER parts = callback_data.split(":") page = int(parts[1]) letter = parts[2] # "A"–"Z" or "ALL" auth_data = await get_user_auth_data(telegram_user_id) jwt_token = auth_data['jwt_token'] client = get_backend_client() async with client: all_companies = await client.get_user_companies(jwt_token=jwt_token) if letter == "ALL": filtered = all_companies else: filtered = [ c for c in all_companies if c.get('name', c.get('nume_firma', '')).upper().startswith(letter) ] from backend.modules.telegram.bot.helpers import create_company_selection_keyboard_paginated keyboard = create_company_selection_keyboard_paginated( filtered, page=page, back_callback="select_company_alpha_menu", page_callback_prefix="select_company_alpha_page", page_callback_suffix=f":{letter}" ) label = f"Firme cu litera {letter}" if letter != "ALL" else "Toate firmele" await query.edit_message_text( f"**{label}** ({len(filtered)}):", reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) elif callback_data == "clients_alpha_menu": # Show A-Z letter filter keyboard for clients from backend.modules.telegram.bot.helpers import create_alpha_filter_keyboard_partner await query.edit_message_text( "**Selecteaza litera**\n\nAlege prima litera a clientului:", reply_markup=create_alpha_filter_keyboard_partner("clients"), parse_mode=ParseMode.MARKDOWN ) elif callback_data.startswith("clients_alpha:"): # Filter clients by starting letter and show page 0 letter = callback_data.split(":", 1)[1] # "A" or "ALL" auth_data = await get_user_auth_data(telegram_user_id) jwt_token = auth_data['jwt_token'] session_manager = get_session_manager() session = await session_manager.get_or_create_session(telegram_user_id) company = session.get_active_company() from backend.modules.telegram.bot.helpers import get_clients_with_maturity clients_data = await get_clients_with_maturity(company['id'], jwt_token) all_clients = clients_data['clients'] if letter == "ALL": filtered = all_clients else: filtered = [ c for c in all_clients if c.get('name', '').upper().startswith(letter) ] if not filtered: await query.answer(f"Niciun client cu litera {letter}.", show_alert=True) return from backend.modules.telegram.bot.menus import create_client_list_keyboard, format_response_with_company from backend.modules.telegram.bot.formatters import format_clients_balance_response content = format_clients_balance_response(filtered, clients_data['maturity']) label = f"Clienti cu litera {letter}" if letter != "ALL" else "Toti clientii" response = format_response_with_company(f"**{label}** ({len(filtered)}):\n\n{content}", company['name']) keyboard = create_client_list_keyboard(filtered, page=0, letter=letter) await query.edit_message_text(response, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN) elif callback_data.startswith("clients_alpha_page:"): # Paginate within an alpha-filtered client list # Callback format: clients_alpha_page:PAGE:LETTER parts = callback_data.split(":") page = int(parts[1]) letter = parts[2] # "A"–"Z" or "ALL" auth_data = await get_user_auth_data(telegram_user_id) jwt_token = auth_data['jwt_token'] session_manager = get_session_manager() session = await session_manager.get_or_create_session(telegram_user_id) company = session.get_active_company() from backend.modules.telegram.bot.helpers import get_clients_with_maturity clients_data = await get_clients_with_maturity(company['id'], jwt_token) all_clients = clients_data['clients'] if letter == "ALL": filtered = all_clients else: filtered = [ c for c in all_clients if c.get('name', '').upper().startswith(letter) ] from backend.modules.telegram.bot.menus import create_client_list_keyboard, format_response_with_company from backend.modules.telegram.bot.formatters import format_clients_balance_response content = format_clients_balance_response(filtered, clients_data['maturity']) label = f"Clienti cu litera {letter}" if letter != "ALL" else "Toti clientii" response = format_response_with_company(f"**{label}** ({len(filtered)}):\n\n{content}", company['name']) keyboard = create_client_list_keyboard(filtered, page=page, letter=letter) await query.edit_message_text(response, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN) elif callback_data == "suppliers_alpha_menu": # Show A-Z letter filter keyboard for suppliers from backend.modules.telegram.bot.helpers import create_alpha_filter_keyboard_partner await query.edit_message_text( "**Selecteaza litera**\n\nAlege prima litera a furnizorului:", reply_markup=create_alpha_filter_keyboard_partner("suppliers"), parse_mode=ParseMode.MARKDOWN ) elif callback_data.startswith("suppliers_alpha:"): # Filter suppliers by starting letter and show page 0 letter = callback_data.split(":", 1)[1] # "A" or "ALL" auth_data = await get_user_auth_data(telegram_user_id) jwt_token = auth_data['jwt_token'] session_manager = get_session_manager() session = await session_manager.get_or_create_session(telegram_user_id) company = session.get_active_company() from backend.modules.telegram.bot.helpers import get_suppliers_with_maturity suppliers_data = await get_suppliers_with_maturity(company['id'], jwt_token) all_suppliers = suppliers_data['suppliers'] if letter == "ALL": filtered = all_suppliers else: filtered = [ s for s in all_suppliers if s.get('name', '').upper().startswith(letter) ] if not filtered: await query.answer(f"Niciun furnizor cu litera {letter}.", show_alert=True) return from backend.modules.telegram.bot.menus import create_supplier_list_keyboard, format_response_with_company from backend.modules.telegram.bot.formatters import format_suppliers_balance_response content = format_suppliers_balance_response(filtered, suppliers_data['maturity']) label = f"Furnizori cu litera {letter}" if letter != "ALL" else "Toti furnizorii" response = format_response_with_company(f"**{label}** ({len(filtered)}):\n\n{content}", company['name']) keyboard = create_supplier_list_keyboard(filtered, page=0, letter=letter) await query.edit_message_text(response, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN) elif callback_data.startswith("suppliers_alpha_page:"): # Paginate within an alpha-filtered supplier list # Callback format: suppliers_alpha_page:PAGE:LETTER parts = callback_data.split(":") page = int(parts[1]) letter = parts[2] # "A"–"Z" or "ALL" auth_data = await get_user_auth_data(telegram_user_id) jwt_token = auth_data['jwt_token'] session_manager = get_session_manager() session = await session_manager.get_or_create_session(telegram_user_id) company = session.get_active_company() from backend.modules.telegram.bot.helpers import get_suppliers_with_maturity suppliers_data = await get_suppliers_with_maturity(company['id'], jwt_token) all_suppliers = suppliers_data['suppliers'] if letter == "ALL": filtered = all_suppliers else: filtered = [ s for s in all_suppliers if s.get('name', '').upper().startswith(letter) ] from backend.modules.telegram.bot.menus import create_supplier_list_keyboard, format_response_with_company from backend.modules.telegram.bot.formatters import format_suppliers_balance_response content = format_suppliers_balance_response(filtered, suppliers_data['maturity']) label = f"Furnizori cu litera {letter}" if letter != "ALL" else "Toti furnizorii" response = format_response_with_company(f"**{label}** ({len(filtered)}):\n\n{content}", company['name']) keyboard = create_supplier_list_keyboard(filtered, page=page, letter=letter) await query.edit_message_text(response, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN) elif callback_data.startswith("select_company:"): # Handle company selection company_id = int(callback_data.split(":")[1]) # Get company details auth_data = await get_user_auth_data(telegram_user_id) jwt_token = auth_data['jwt_token'] client = get_backend_client() async with client: companies = await client.get_user_companies(jwt_token=jwt_token) # Find selected company selected = next( (c for c in companies if c.get('id_firma', c.get('id')) == company_id), None ) if selected: # Set active company in session session_manager = get_session_manager() session = await session_manager.get_or_create_session(telegram_user_id) # Extract company data with backwards compatibility company_name = selected.get('name', selected.get('nume_firma', 'N/A')) company_cui = selected.get('fiscal_code', selected.get('cui')) session.set_active_company( company_id=company_id, company_name=company_name, company_cui=company_cui ) await session_manager.save_session(telegram_user_id) # Show main menu directly (no confirmation message) from backend.modules.telegram.bot.menus import create_main_menu, get_menu_message keyboard = create_main_menu( company_name=company_name, company_cui=company_cui ) menu_text = get_menu_message(company_name, company_cui) await query.edit_message_text( menu_text, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) else: await query.edit_message_text( "Companie negasita sau nu ai acces la ea." ) # ========== SWITCH SERVER CALLBACKS ========== elif callback_data.startswith("switch_server_confirm:"): new_server_id = callback_data.split(":", 1)[1] telegram_user_id = update.effective_user.id auth_data = await get_user_auth_data(telegram_user_id) if not auth_data: await query.edit_message_text("Sesiune expirată. Re-autentifică-te cu /login") return # Stochează serverul țintă și cere parola — servere diferite pot avea parole diferite context.user_data['pending_switch_server_id'] = new_server_id context.user_data['pending_switch_jwt_token'] = auth_data['jwt_token'] context.user_data['pending_switch_username'] = auth_data['username'] try: from backend.config import settings srv = settings.get_oracle_server(new_server_id) srv_display = srv.name if srv else new_server_id except Exception: srv_display = new_server_id from telegram import ForceReply await query.edit_message_text( f"🔐 **Schimbare server: {srv_display}**\n\n" f"Introdu parola Oracle pentru acest server:\n" f"_(Mesajul cu parola va fi șters automat)_", parse_mode=ParseMode.MARKDOWN ) # Trimite un mesaj separat cu ForceReply pentru a forța input-ul await context.bot.send_message( chat_id=update.effective_chat.id, text="Parolă:", reply_markup=ForceReply(selective=True, input_field_placeholder="Parola Oracle...") ) return # ========== LOGOUT CALLBACKS ========== elif callback_data == "logout_confirm": # Logout user (same as unlink but shows menu after) from backend.modules.telegram.auth.linking import unlink_user success = await unlink_user(telegram_user_id) if success: # Clear session too session_manager = get_session_manager() await session_manager.delete_session(telegram_user_id) # Show login menu (non-authenticated) from backend.modules.telegram.bot.menus import create_main_menu, get_menu_message, pad_message_for_wide_buttons keyboard = create_main_menu(company_name=None, company_cui=None, is_authenticated=False) menu_text = pad_message_for_wide_buttons( "**Deconectat cu succes**\n\n" "Contul tău a fost deconectat.\n\n" "Pentru a te reconecta, apasă **Login**." ) await query.edit_message_text( menu_text, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) else: await query.edit_message_text( "A apărut o eroare la deconectare.\n" "Te rog încearcă din nou." ) elif callback_data == "logout_cancel": # Cancel logout - return to main menu session_manager = get_session_manager() session = await session_manager.get_or_create_session(telegram_user_id) company = session.get_active_company() # Check auth auth_data = await get_user_auth_data(telegram_user_id) is_authenticated = auth_data is not None from backend.modules.telegram.bot.menus import create_main_menu, get_menu_message company_name = company['name'] if company else None company_cui = company.get('cui') if company else None keyboard = create_main_menu(company_name, company_cui, is_authenticated) menu_text = get_menu_message(company_name, company_cui) await query.edit_message_text( menu_text, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) # ========== UNLINK CALLBACKS (LEGACY) ========== elif callback_data == "unlink_confirm": # Unlink user from backend.modules.telegram.auth.linking import unlink_user success = await unlink_user(telegram_user_id) if success: # Clear session too session_manager = get_session_manager() await session_manager.delete_session(telegram_user_id) await query.edit_message_text( "**Cont deconectat cu succes**\n\n" "Datele tale au fost șterse din sistem.\n\n" "Pentru a te reconecta, folosește `/start CODUL_TAU`", parse_mode=ParseMode.MARKDOWN ) else: await query.edit_message_text( "A aparut o eroare la deconectare.\n" "Te rog incearca din nou." ) elif callback_data == "unlink_cancel": await query.edit_message_text( "Deconectare anulata.\n\n" "Contul tau ramane linkuit." ) # ========== LOGIN CALLBACKS ========== elif callback_data == "login_help": # Show detailed help on how to get link code await query.edit_message_text( "**Cum obțin codul de link?**\n\n" "1. Loghează-te în aplicația web ROA2WEB\n" "2. Mergi la: Setări → Telegram Linking\n" "3. Apasă **Generează Cod**\n" "4. Vei primi un cod din 8 caractere (ex: ABC12XYZ)\n" "5. Trimite-mi comanda: `/start CODUL_TAU`\n\n" "**Important:** Codul expiră în 15 minute.", reply_markup=InlineKeyboardMarkup([ [InlineKeyboardButton("Am deja cod - Linkez acum", callback_data="login_prompt")], [InlineKeyboardButton("« Înapoi la Meniu", callback_data="action:menu")] ]), parse_mode=ParseMode.MARKDOWN ) elif callback_data == "login_prompt": # Prompt user to enter link code directly from telegram import ForceReply await query.edit_message_text( "**Conectare Cont ROA2WEB**\n\n" "Trimite-mi codul primit din aplicatia web.\n\n" "Poti trimite:\n" "- Doar codul: ABC12XYZ\n" "- Sau comanda: /start ABC12XYZ\n\n" "Codul expira in 15 minute.", parse_mode=ParseMode.MARKDOWN ) # Send a follow-up message with ForceReply to prompt input await context.bot.send_message( chat_id=telegram_user_id, text="Scrie sau lipeste codul aici:", reply_markup=ForceReply( selective=True, input_field_placeholder="ABC12XYZ" ) ) elif callback_data == "login_back": # Go back to welcome message keyboard = InlineKeyboardMarkup([ [InlineKeyboardButton("Cum obtin codul de link?", callback_data="login_help")], [InlineKeyboardButton("Am deja cod - Linkez contul", callback_data="login_prompt")] ]) await query.edit_message_text( "**Bun venit la ROA2WEB Bot!**\n\n" "Sunt asistentul tau financiar pentru sistemul ERP ROA2WEB.\n\n" "**Pentru a incepe, trebuie sa-ti linkezi contul Telegram cu contul tau ROA2WEB.**\n\n" "Alege o optiune:", reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) # ========== NEW CALLBACKS (FAZA 4) ========== # NIVEL 1: Main Menu Buttons elif callback_data.startswith("menu:"): await handle_menu_callback(query, telegram_user_id, callback_data) # Action Buttons elif callback_data.startswith("action:"): await handle_action_callback(query, telegram_user_id, callback_data) # NIVEL 2: Client/Supplier Details elif callback_data.startswith("details:"): await handle_details_callback(query, telegram_user_id, callback_data) # NIVEL 3: Invoice Details elif callback_data.startswith("invoice:"): await handle_invoice_callback(query, telegram_user_id, callback_data) # Navigation Back elif callback_data.startswith("nav:back:"): await handle_navigation_back(query, telegram_user_id, callback_data) # ========== CACHE CALLBACKS (FAZA 6) ========== elif callback_data.startswith("cache_toggle:"): # Handle cache toggle button action = callback_data.split(":")[1] enabled = action == "on" auth_data = await get_user_auth_data(telegram_user_id) jwt_token = auth_data['jwt_token'] try: client = get_backend_client() async with client: await client.toggle_user_cache(jwt_token=jwt_token, enabled=enabled) status = "activat" if enabled else "dezactivat" message = f"✅ **Cache {status}** pentru tine.\n\n" if enabled: message += "Queries vor fi servite din cache când e posibil." else: message += "Toate queries vor merge direct la Oracle.\nFolosește /togglecache din nou pentru reactivare." # Add back button keyboard = InlineKeyboardMarkup([ [InlineKeyboardButton("« Înapoi la Meniu", callback_data="action:menu")] ]) await query.edit_message_text( message, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) except Exception as e: logger.error(f"Toggle cache callback error: {e}", exc_info=True) await query.answer("❌ Eroare la modificarea setării cache.", show_alert=True) elif callback_data.startswith("cache_clear:"): # Handle clear cache button scope = callback_data.split(":")[1] auth_data = await get_user_auth_data(telegram_user_id) jwt_token = auth_data['jwt_token'] try: client = get_backend_client() if scope == "all": # Clear all cache async with client: await client.invalidate_cache(jwt_token=jwt_token, company_id=None) message = "✅ Cache invalidat pentru **toate companiile**.\n\nDatele vor fi refreshate la următoarea interogare." elif scope == "current": # Clear only current company session_manager = get_session_manager() session = await session_manager.get_or_create_session(telegram_user_id) company = session.get_active_company() if not company: await query.answer("Nu ai o companie selectată.", show_alert=True) return async with client: await client.invalidate_cache(jwt_token=jwt_token, company_id=company['id']) message = f"✅ Cache invalidat pentru **{company['name']}**.\n\nDatele vor fi refreshate la următoarea interogare." # Add back button keyboard = InlineKeyboardMarkup([ [InlineKeyboardButton("« Înapoi la Meniu", callback_data="action:menu")] ]) await query.edit_message_text( message, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) except Exception as e: logger.error(f"Clear cache callback error: {e}", exc_info=True) await query.answer("❌ Eroare la ștergerea cache-ului.", show_alert=True) # ========== PAGINATION CALLBACKS ========== elif callback_data.startswith("clients_page:"): # Handle clients pagination page = int(callback_data.split(":")[1]) # Get auth data and company auth_data = await get_user_auth_data(telegram_user_id) jwt_token = auth_data['jwt_token'] session_manager = get_session_manager() session = await session_manager.get_or_create_session(telegram_user_id) company = session.get_active_company() # Get clients with maturity from backend.modules.telegram.bot.helpers import get_clients_with_maturity clients_data = await get_clients_with_maturity(company['id'], jwt_token) from backend.modules.telegram.bot.formatters import format_clients_balance_response from backend.modules.telegram.bot.menus import create_client_list_keyboard, format_response_with_company content = format_clients_balance_response( clients_data['clients'], clients_data['maturity'] ) response = format_response_with_company(content, company['name']) keyboard = create_client_list_keyboard(clients_data['clients'], page=page) await query.edit_message_text( response, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) elif callback_data.startswith("suppliers_page:"): # Handle suppliers pagination page = int(callback_data.split(":")[1]) # Get auth data and company auth_data = await get_user_auth_data(telegram_user_id) jwt_token = auth_data['jwt_token'] session_manager = get_session_manager() session = await session_manager.get_or_create_session(telegram_user_id) company = session.get_active_company() # Get suppliers with maturity from backend.modules.telegram.bot.helpers import get_suppliers_with_maturity suppliers_data = await get_suppliers_with_maturity(company['id'], jwt_token) from backend.modules.telegram.bot.formatters import format_suppliers_balance_response from backend.modules.telegram.bot.menus import create_supplier_list_keyboard, format_response_with_company content = format_suppliers_balance_response( suppliers_data['suppliers'], suppliers_data['maturity'] ) response = format_response_with_company(content, company['name']) keyboard = create_supplier_list_keyboard(suppliers_data['suppliers'], page=page) await query.edit_message_text( response, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) elif callback_data.startswith("invoices_page:"): # Handle invoices pagination # Format: invoices_page:PARTNER_TYPE:PARTNER_NAME:PAGE parts = callback_data.split(":") partner_type = parts[1] # CLIENTI or FURNIZORI partner_name = parts[2] page = int(parts[3]) # Get auth data and company auth_data = await get_user_auth_data(telegram_user_id) jwt_token = auth_data['jwt_token'] session_manager = get_session_manager() session = await session_manager.get_or_create_session(telegram_user_id) company = session.get_active_company() # Get invoices for this partner if partner_type == "CLIENTI": from backend.modules.telegram.bot.helpers import get_client_invoices, get_clients_with_maturity invoices = await get_client_invoices(company['id'], partner_name, jwt_token) # Get client details clients_data = await get_clients_with_maturity(company['id'], jwt_token) partner = next((c for c in clients_data['clients'] if c['name'] == partner_name), None) from backend.modules.telegram.bot.formatters import format_client_detail_response content = format_client_detail_response(partner, invoices) else: from backend.modules.telegram.bot.helpers import get_supplier_invoices, get_suppliers_with_maturity invoices = await get_supplier_invoices(company['id'], partner_name, jwt_token) # Get supplier details suppliers_data = await get_suppliers_with_maturity(company['id'], jwt_token) partner = next((s for s in suppliers_data['suppliers'] if s['name'] == partner_name), None) from backend.modules.telegram.bot.formatters import format_supplier_detail_response content = format_supplier_detail_response(partner, invoices) from backend.modules.telegram.bot.menus import create_invoice_list_keyboard, format_response_with_company response = format_response_with_company(content, company['name']) keyboard = create_invoice_list_keyboard(invoices, partner_type, partner_name, page=page) await query.edit_message_text( response, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) elif callback_data == "noop": # No operation - just acknowledge pass except Exception as e: logger.error(f"Error in button_callback: {e}", exc_info=True) # ============================================================================ # ERROR HANDLER # ============================================================================ async def error_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): """ Handle errors in bot operations. Args: update: Telegram update object context: Telegram context with error """ logger.error(f"Update {update} caused error {context.error}", exc_info=context.error) # Try to send error message to user try: if update and update.effective_message: await update.effective_message.reply_text( "**A aparut o eroare tehnica**\n\n" "Te rog incearca din nou sau contacteaza support.\n\n" "Daca problema persista, foloseste /clear pentru a reseta conversatia.", parse_mode=ParseMode.MARKDOWN ) except Exception as e: logger.error(f"Failed to send error message to user: {e}") # ============================================================================ # COMMON HANDLER FUNCTIONS (pentru consistență comenzi/butoane) # ============================================================================ async def _handle_expired_auth(query_or_update, telegram_user_id: int, auth_data: Optional[Dict[str, Any]]) -> bool: """ Check if auth_data is None (token expired/refresh failed) and send user-friendly message. Args: query_or_update: Update (command) or CallbackQuery (button) telegram_user_id: Telegram user ID auth_data: Authentication data from get_user_auth_data() Returns: True if auth is expired (stop execution), False if auth is valid (continue) """ if auth_data is None: logger.warning(f"Auth expired for user {telegram_user_id}, sending re-authentication message") # Create re-authentication message message = ( "⚠️ **Sesiunea ta a expirat**\n\n" "Token-ul de autentificare a expirat și nu a putut fi reînnoit automat.\n\n" "**Pentru a continua:**\n" "1. Accesează aplicația web ROA2WEB\n" "2. Loginează-te cu contul tău Oracle\n" "3. Generează un nou cod de link pentru Telegram\n" "4. Trimite comanda `/start CODUL_TAU`\n\n" "_Sau folosește `/unlink` pentru a deconecta contul actual._" ) # Send message based on source type from telegram import CallbackQuery if isinstance(query_or_update, CallbackQuery): # It's a button callback - transform menu to Login menu await query_or_update.answer("Sesiunea a expirat. Te rog să te reconectezi.", show_alert=True) # Transform the current message (menu) to Login menu from backend.modules.telegram.bot.menus import create_main_menu, pad_message_for_wide_buttons keyboard = create_main_menu(company_name=None, company_cui=None, is_authenticated=False) menu_text = pad_message_for_wide_buttons( "⚠️ **Sesiunea a expirat**\n\n" "Pentru a continua, apasă **Login**\n" "și urmează instrucțiunile." ) await query_or_update.edit_message_text( menu_text, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) else: # It's a command (Update) - just send message await query_or_update.message.reply_text(message, parse_mode=ParseMode.MARKDOWN) return True # Stop execution return False # Continue execution async def _handle_sold_view( query_or_update, telegram_user_id: int, company: Dict[str, Any], jwt_token: str, is_callback: bool = False ): """ Common handler pentru sold view (dashboard). Folosit de: - Comanda /dashboard - Comanda /sold - Butonul menu:sold Args: query_or_update: Query (callback) sau Update (command) telegram_user_id: Telegram user ID company: Dict cu id, name, cui jwt_token: JWT token is_callback: True dacă e apelat din callback, False dacă e command """ try: client = get_backend_client() async with client: data = await client.get_dashboard_data( company_id=company['id'], jwt_token=jwt_token ) if not data: error_msg = "Eroare la incarcarea dashboard-ului." if is_callback: await query_or_update.edit_message_text(error_msg) else: await query_or_update.message.reply_text(error_msg) return from backend.modules.telegram.bot.formatters import format_dashboard_response, add_performance_footer from backend.modules.telegram.bot.menus import create_action_buttons, format_response_with_company # Extract cache metadata cache_hit = data.get('cache_hit', False) response_time_ms = data.get('response_time_ms', 0) cache_source = data.get('cache_source', None) content = format_dashboard_response(data) response = format_response_with_company(content, company['name']) # Add performance footer if response_time_ms > 0: response = add_performance_footer(response, cache_hit, response_time_ms, cache_source) keyboard = create_action_buttons("sold", show_export=False, show_refresh=False) if is_callback: await query_or_update.edit_message_text( response, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) else: await query_or_update.message.reply_text( response, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) except Exception as e: logger.error(f"Error in _handle_sold_view: {e}", exc_info=True) error_msg = "Eroare la incarcarea dashboard-ului." if is_callback: await query_or_update.answer(error_msg, show_alert=True) else: await query_or_update.message.reply_text(error_msg) async def _handle_selectcompany_view( query_or_update, telegram_user_id: int, jwt_token: str, is_callback: bool = False, page: int = 0, search_term: str = "" ): """ Common handler pentru company selection cu paginare. Folosit de: - Comanda /selectcompany - Butonul menu:select_company - Callback-urile de paginare (select_company_page:N) Args: query_or_update: Query (callback) sau Update (command) telegram_user_id: Telegram user ID jwt_token: JWT token is_callback: True dacă e apelat din callback, False dacă e command page: Numărul paginii (0-indexed) search_term: Termen de căutare (opțional) """ try: client = get_backend_client() async with client: companies = await client.get_user_companies(jwt_token=jwt_token) # Apply search filter if provided if search_term: from backend.modules.telegram.bot.helpers import search_companies_by_name companies = await search_companies_by_name(search_term, jwt_token) if not companies: error_msg = f"Nu am gasit companii care contin '{search_term}'.\n\n" \ "Incearca alt termen sau /selectcompany pentru lista completa." if is_callback: await query_or_update.answer(error_msg, show_alert=True) else: await query_or_update.message.reply_text(error_msg) return if not companies: error_msg = "Nu ai acces la nicio companie.\nContacteaza administratorul." if is_callback: await query_or_update.edit_message_text( error_msg, parse_mode=ParseMode.MARKDOWN ) else: await query_or_update.message.reply_text( error_msg, parse_mode=ParseMode.MARKDOWN ) return # Auto-selectează dacă există exact o singură firmă if len(companies) == 1: selected = companies[0] company_id = selected.get('id_firma', selected.get('id')) company_name = selected.get('name', selected.get('nume_firma', 'N/A')) company_cui = selected.get('fiscal_code', selected.get('cui')) session_manager = get_session_manager() session = await session_manager.get_or_create_session(telegram_user_id) session.set_active_company( company_id=company_id, company_name=company_name, company_cui=company_cui ) await session_manager.save_session(telegram_user_id) from backend.modules.telegram.bot.menus import create_main_menu, get_menu_message keyboard = create_main_menu(company_name=company_name, company_cui=company_cui) menu_text = f"✅ Firmă selectată automat: **{company_name}**\n\n" + get_menu_message(company_name, company_cui) if is_callback: await query_or_update.edit_message_text( menu_text, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) else: await query_or_update.message.reply_text( menu_text, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) return from backend.modules.telegram.bot.helpers import create_company_selection_keyboard_paginated keyboard = create_company_selection_keyboard_paginated(companies, page=page) message = f"**Selecteaza Compania**\n\n" if search_term: message += f"Rezultate '{search_term}' ({len(companies)}):" else: message += f"Companiile tale ({len(companies)}):" if is_callback: await query_or_update.edit_message_text( message, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) else: await query_or_update.message.reply_text( message, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) except Exception as e: logger.error(f"Error in _handle_selectcompany_view: {e}", exc_info=True) error_msg = "A aparut o eroare. Te rog incearca din nou." if is_callback: await query_or_update.answer(error_msg, show_alert=True) else: await query_or_update.message.reply_text(error_msg) # Export all handlers __all__ = [ 'start_command', 'help_command', 'clear_command', 'companies_command', 'unlink_command', 'selectcompany_command', 'dashboard_command', 'sold_command', 'facturi_command', 'trezorerie_command', # FAZA 3: New command handlers with button interface 'menu_command', 'trezorerie_casa_command', 'trezorerie_banca_command', 'clienti_command', 'furnizori_command', 'evolutie_command', # Text message handlers 'handle_text_message', # FAZA 4: Callback helper functions 'handle_menu_callback', 'handle_action_callback', 'handle_details_callback', 'handle_invoice_callback', 'handle_navigation_back', # Callback and error handlers 'button_callback', 'error_handler', # Common handler functions '_handle_sold_view', '_handle_selectcompany_view' ]