Files
roa2web-service-auto/reports-app/telegram-bot/app/bot/handlers.py
Marius Mutu 6b13ffa183 Initial commit: ROA2WEB - FastAPI + Vue.js + Telegram Bot
Modern ERP Reports Application with microservices architecture

Tech Stack:
- Backend: FastAPI + python-oracledb (Oracle DB integration)
- Frontend: Vue.js 3 + PrimeVue + Vite
- Telegram Bot: python-telegram-bot + SQLite
- Infrastructure: Shared database pool, JWT authentication, SSH tunnel

Features:
- FastAPI backend with async Oracle connection pool
- Vue.js 3 responsive frontend with PrimeVue components
- Telegram bot alternative interface
- Microservices architecture with shared components
- Complete deployment support (Linux Docker + Windows IIS)
- Comprehensive testing (Playwright E2E + pytest)

Repository Structure:
- reports-app/ - Main application (backend, frontend, telegram-bot)
- shared/ - Shared components (database pool, auth, utils)
- deployment/ - Deployment scripts (Linux & Windows)
- docs/ - Project documentation
- security/ - Security scanning and git hooks
2025-10-25 14:55:08 +03:00

2037 lines
73 KiB
Python

"""
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 <auth_code> - 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 <auth_code> - 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']
companies = result.get('companies', [])
companies_text = ""
if companies:
companies_text = "\n\n**Companiile tale:**\n"
for comp_id in companies[:3]: # Show first 3 (companies are IDs as strings)
companies_text += f"- Companie ID: {comp_id}\n"
if len(companies) > 3:
companies_text += f"- ... si inca {len(companies) - 3} companii\n"
await update.message.reply_text(
f"**Cont conectat cu succes**\n\n"
f"Bun venit, **{username}**\n"
f"{companies_text}\n"
f"Foloseste meniul sau /help pentru comenzi.",
parse_mode=ParseMode.MARKDOWN
)
# 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, get_menu_message
keyboard = create_main_menu(company_name, company_cui)
menu_text = get_menu_message(company_name, company_cui)
await update.message.reply_text(
menu_text,
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"
"Genereaza un cod nou din aplicatia web si trimite:\n"
"/start <cod>\n\n"
"Codul expira in 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
# Create main menu
from app.bot.menus import create_main_menu, get_menu_message
keyboard = create_main_menu(company_name)
menu_text = get_menu_message(company_name)
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. Logheaza-te in aplicatia web ROA2WEB
2. Acceseaza Setari > Telegram Linking
3. Genereaza cod (valabil 15 minute)
4. Trimite codul in Telegram: /start <cod>
**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
from app.bot.menus import create_main_menu, get_menu_message
keyboard = create_main_menu(company_name, company_cui)
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)
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']
companies = result.get('companies', [])
companies_text = ""
if companies:
companies_text = "\n\n**Companiile tale:**\n"
for comp_id in companies[:3]: # Show first 3
companies_text += f"- Companie ID: {comp_id}\n"
if len(companies) > 3:
companies_text += f"- ... si inca {len(companies) - 3} companii\n"
await update.message.reply_text(
f"**Cont conectat cu succes**\n\n"
f"Bun venit, **{username}**\n"
f"{companies_text}\n"
f"Foloseste meniul sau /help pentru comenzi.",
parse_mode=ParseMode.MARKDOWN
)
# 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, get_menu_message
keyboard = create_main_menu(company_name, company_cui)
menu_text = get_menu_message(company_name, company_cui)
await update.message.reply_text(
menu_text,
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)
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=True)
await query.edit_message_text(
response,
reply_markup=keyboard,
parse_mode=ParseMode.MARKDOWN
)
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=True)
await query.edit_message_text(
response,
reply_markup=keyboard,
parse_mode=ParseMode.MARKDOWN
)
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)
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).
Callback format: action:{type}:{view}
Types: refresh, export, menu
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()
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)
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
await handle_details_callback(query, telegram_user_id, f"details:client:{entity_name}:0")
elif view.startswith("supplier_detail:"):
entity_name = view.split(":", 1)[1] # Extract entity name
await handle_details_callback(query, telegram_user_id, f"details:supplier:{entity_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)
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 invoices by name
from app.bot.helpers import get_client_invoices
invoices = await get_client_invoices(company['id'], entity_name, jwt_token)
# Get client details (from clients list)
from app.bot.helpers import get_clients_with_maturity
clients_data = await get_clients_with_maturity(company['id'], jwt_token)
client = next((c for c in clients_data['clients'] if c['name'] == entity_name), None)
if not client:
await query.answer("Client negasit", show_alert=True)
return
# 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'])
keyboard = create_action_buttons(f"client_detail:{entity_name}", show_export=False, show_back=True)
await query.edit_message_text(
response,
reply_markup=keyboard,
parse_mode=ParseMode.MARKDOWN
)
elif detail_type == "supplier":
# Get supplier invoices by name
from app.bot.helpers import get_supplier_invoices
invoices = await get_supplier_invoices(company['id'], entity_name, jwt_token)
# Get supplier details (from suppliers list)
from app.bot.helpers import get_suppliers_with_maturity
suppliers_data = await get_suppliers_with_maturity(company['id'], jwt_token)
supplier = next((s for s in suppliers_data['suppliers'] if s['name'] == entity_name), None)
if not supplier:
await query.answer("Furnizor negasit", show_alert=True)
return
# 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'])
keyboard = create_action_buttons(f"supplier_detail:{entity_name}", show_export=False, show_back=True)
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."
)
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 sterse din sistem.\n\n"
"Pentru a te reconecta, foloseste /start <cod>",
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 obtin codul de link?**\n\n"
"1. Logheaza-te in aplicatia web ROA2WEB\n"
"2. Mergi la: Setari -> Telegram Linking\n"
"3. Apasa \"Genereaza Cod\"\n"
"4. Vei primi un cod din 8 caractere (ex: ABC12XYZ)\n"
"5. Trimite-mi comanda: /start <codul_tau>\n\n"
"Important: Codul expira in 15 minute.",
reply_markup=InlineKeyboardMarkup([
[InlineKeyboardButton("Am deja cod - Linkez acum", callback_data="login_prompt")],
[InlineKeyboardButton("« Inapoi la Bun Venit", callback_data="login_back")]
]),
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_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=True)
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'
]