""" 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 app.auth.linking import ( link_telegram_account, check_user_linked, get_user_auth_data, get_user_companies ) from app.agent.session import get_session_manager from app.db.operations import update_user_last_active from app.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}") # Show "linking..." message linking_msg = await update.message.reply_text( "Linking contul...\n" "Te rog asteapta..." ) # Attempt linking result = await link_telegram_account(telegram_user, auth_code) # Delete "linking..." message await linking_msg.delete() 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 app.bot.menus import create_main_menu, pad_message_for_wide_buttons keyboard = create_main_menu(company_name, company_cui, is_authenticated=True) # Single welcome message with menu if company_name: welcome_text = ( f"**Cont conectat cu succes**\n\n" f"Bun venit, **{username}**!\n\n" f"{company_name}" ) else: welcome_text = ( f"**Cont conectat cu succes**\n\n" f"Bun venit, **{username}**!\n\n" f"Selectează o companie pentru a continua" ) welcome_message = pad_message_for_wide_buttons(welcome_text) await update.message.reply_text( welcome_message, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) logger.info(f"User {telegram_user_id} successfully linked to {username}") else: # Failed linking await update.message.reply_text( "**Cod invalid sau expirat**\n\n" "Generează un cod nou din aplicația web și trimite:\n" "`/start CODUL_TAU`\n\n" "Codul expiră în 15 minute.", 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 app.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 instructions with interactive buttons 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( "**Bun venit la ROA2WEB Bot**\n\n" "Pentru a incepe, conecteaza contul tau ROA2WEB.\n\n" "Alege o optiune:", 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) **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 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 app.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 app.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 app.bot.formatters import format_invoices_response response = format_invoices_response(invoices, company['name']) # FAZA 3: Add action buttons from app.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 app.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 app.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 # 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 app.bot.menus import format_response_with_company text = format_response_with_company(content, company['name']) # 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 # Create main menu (user is authenticated if they passed the is_linked check) from app.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( 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 app.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 app.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 app.bot.formatters import format_treasury_casa_response from app.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']) 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 app.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 app.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 app.bot.formatters import format_treasury_banca_response from app.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']) 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 app.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 app.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 # Format response from app.bot.formatters import format_clients_balance_response from app.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=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 app.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 app.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 # Format response from app.bot.formatters import format_suppliers_balance_response from app.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=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 app.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 app.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 app.bot.formatters import format_cashflow_evolution_response from app.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']) 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().upper() logger.info(f"Text message from user {telegram_user_id}: {text}") # 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 # 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}") # Show "linking..." message linking_msg = await update.message.reply_text( "Linking contul...\n" "Te rog asteapta..." ) # Attempt linking result = await link_telegram_account(telegram_user, text) # Delete "linking..." message await linking_msg.delete() 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 app.bot.menus import create_main_menu, pad_message_for_wide_buttons keyboard = create_main_menu(company_name, company_cui, is_authenticated=True) # Single welcome message with menu if company_name: welcome_text = ( f"**Cont conectat cu succes**\n\n" f"Bun venit, **{username}**!\n\n" f"{company_name}" ) else: welcome_text = ( f"**Cont conectat cu succes**\n\n" f"Bun venit, **{username}**!\n\n" f"Selectează o companie pentru a continua" ) welcome_message = pad_message_for_wide_buttons(welcome_text) await update.message.reply_text( welcome_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 await update.message.reply_text( "**Cod invalid sau expirat**\n\n" "Genereaza un cod nou din aplicatia web si trimite-l direct.\n\n" "Codul expira in 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 app.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 app.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 == "casa": # Trezorerie casa from app.bot.helpers import get_treasury_breakdown_split treasury_data = await get_treasury_breakdown_split(company['id'], jwt_token) from app.bot.formatters import format_treasury_casa_response from app.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']) 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 app.bot.helpers import get_treasury_breakdown_split treasury_data = await get_treasury_breakdown_split(company['id'], jwt_token) from app.bot.formatters import format_treasury_banca_response from app.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']) 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 app.bot.helpers import get_clients_with_maturity clients_data = await get_clients_with_maturity(company['id'], jwt_token) from app.bot.formatters import format_clients_balance_response from app.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=0) await query.edit_message_text( response, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) elif action == "furnizori": # Sold furnizori + listă cu paginare from app.bot.helpers import get_suppliers_with_maturity suppliers_data = await get_suppliers_with_maturity(company['id'], jwt_token) from app.bot.formatters import format_suppliers_balance_response from app.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=0) await query.edit_message_text( response, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) elif action == "evolutie": # Evoluție cash flow from app.bot.helpers import get_cashflow_evolution_data evolution_data = await get_cashflow_evolution_data(company['id'], jwt_token) from app.bot.formatters import format_cashflow_evolution_response from app.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']) 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 == "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="" ) 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 from app.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 ) 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 app.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 app.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 ) elif action_type == "login": # Prompt user to enter link code directly (same as login_prompt functionality) from telegram import ForceReply from app.bot.menus import pad_message_for_wide_buttons # Edit the current message with instructions login_text = pad_message_for_wide_buttons( "**Conectare Cont ROA2WEB**\n\n" "Trimite-mi codul primit din aplicația web.\n\n" "Poți trimite:\n" "• Doar codul: ABC12XYZ\n" "• Sau comanda: /start ABC12XYZ\n\n" "Codul expiră în 15 minute." ) # Buttons for help or cancel keyboard = InlineKeyboardMarkup([ [InlineKeyboardButton("Cum obțin codul?", callback_data="login_help")], [InlineKeyboardButton("« Anulează", callback_data="action:menu")] ]) await query.edit_message_text( login_text, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN ) # Send a follow-up message with ForceReply to prompt input await query.message.reply_text( "Scrie sau lipește codul aici:", reply_markup=ForceReply( selective=True, input_field_placeholder="ABC12XYZ" ) ) 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 app.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 app.bot.helpers import get_client_invoices invoices = await get_client_invoices(company['id'], full_client_name, jwt_token) # Format response from app.bot.formatters import format_client_detail_response from app.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 app.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 app.bot.helpers import get_supplier_invoices invoices = await get_supplier_invoices(company['id'], full_supplier_name, jwt_token) # Format response from app.bot.formatters import format_supplier_detail_response from app.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 await query.answer() telegram_user_id = update.effective_user.id callback_data = query.data 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 app.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.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 app.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." ) # ========== LOGOUT CALLBACKS ========== elif callback_data == "logout_confirm": # Logout user (same as unlink but shows menu after) from app.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 app.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 app.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 app.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) # ========== 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 app.bot.helpers import get_clients_with_maturity clients_data = await get_clients_with_maturity(company['id'], jwt_token) from app.bot.formatters import format_clients_balance_response from app.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 app.bot.helpers import get_suppliers_with_maturity suppliers_data = await get_suppliers_with_maturity(company['id'], jwt_token) from app.bot.formatters import format_suppliers_balance_response from app.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 app.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 app.bot.formatters import format_client_detail_response content = format_client_detail_response(partner, invoices) else: from app.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 app.bot.formatters import format_supplier_detail_response content = format_supplier_detail_response(partner, invoices) from app.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 app.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 app.bot.formatters import format_dashboard_response from app.bot.menus import create_action_buttons, format_response_with_company content = format_dashboard_response(data) response = format_response_with_company(content, company['name']) 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 app.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 from app.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' ]