Implements cache tier identification in Telegram bot to display data source: - "db" for database queries - "cached L1" for in-memory cache hits - "cached L2" for SQLite cache hits Backend changes: - Added cache metadata fields to TrendsResponse and DashboardSummary models (cache_hit, response_time_ms, cache_source) - Updated /api/dashboard/summary and /api/dashboard/trends endpoints to include cache metadata when X-Include-Cache-Metadata header is present - Cache metadata is extracted from request.state (set by @cached decorator) Telegram bot changes: - Updated API client to send X-Include-Cache-Metadata header - Modified helpers to extract cache_source from backend responses - Updated handlers to pass cache metadata to formatters - Performance footer now displays specific cache tier (L1 vs L2) Fixed Pydantic serialization issue: - Changed field names from _cache_hit to cache_hit (without underscore) - Pydantic excludes underscore-prefixed fields from JSON by default 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
2740 lines
103 KiB
Python
2740 lines
103 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']
|
|
jwt_token = result['jwt_token']
|
|
|
|
# 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
|
|
|
|
# Get cache status
|
|
cache_enabled = None
|
|
try:
|
|
from app.api.client import get_backend_client
|
|
client = get_backend_client()
|
|
async with client:
|
|
cache_stats = await client.get_cache_stats(jwt_token=jwt_token)
|
|
cache_enabled = cache_stats.get('user_enabled', True)
|
|
except Exception as e:
|
|
logger.warning(f"Could not get cache status in /start: {e}")
|
|
|
|
from app.bot.menus import create_main_menu, pad_message_for_wide_buttons
|
|
keyboard = create_main_menu(company_name, company_cui, is_authenticated=True, cache_enabled=cache_enabled)
|
|
|
|
# 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)
|
|
|
|
**Comenzi Cache (optimizare performanta):**
|
|
/togglecache - Activeaza/Dezactiveaza cache pentru tine
|
|
/clearcache - Sterge cache pentru compania activa
|
|
/clearcache all - Sterge tot cache-ul
|
|
|
|
**Conectare cont:**
|
|
1. Loghează-te în aplicația web ROA2WEB
|
|
2. Accesează Setări → Telegram Linking
|
|
3. Generează cod (valabil 15 minute)
|
|
4. Trimite codul în Telegram: `/start CODUL_TAU`
|
|
|
|
**Securitate:**
|
|
Datele sunt protejate prin autentificare JWT.
|
|
Poti deconecta oricand cu /unlink.
|
|
"""
|
|
|
|
await update.message.reply_text(help_text, parse_mode=ParseMode.MARKDOWN)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in help_command: {e}", exc_info=True)
|
|
await update.message.reply_text(
|
|
"A aparut o eroare. Te rog incearca din nou."
|
|
)
|
|
|
|
|
|
async def clear_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""
|
|
Handle /clear command.
|
|
|
|
Clears the active company selection for the user.
|
|
|
|
Args:
|
|
update: Telegram update object
|
|
context: Telegram context
|
|
"""
|
|
try:
|
|
telegram_user_id = update.effective_user.id
|
|
|
|
logger.info(f"/clear command from user {telegram_user_id}")
|
|
|
|
# Clear active company from session
|
|
session_manager = get_session_manager()
|
|
session = await session_manager.get_or_create_session(telegram_user_id)
|
|
session.clear_active_company()
|
|
await session_manager.save_session(telegram_user_id)
|
|
|
|
await update.message.reply_text(
|
|
"**Companie activa stearsa**\n\n"
|
|
"Foloseste /selectcompany pentru a selecta alta companie.",
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in clear_command: {e}", exc_info=True)
|
|
await update.message.reply_text(
|
|
"A apărut o eroare la ștergerea companiei active."
|
|
)
|
|
|
|
|
|
async def clearcache_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""
|
|
Handle /clearcache command.
|
|
|
|
Clears the cache for the current company or all companies.
|
|
|
|
Usage:
|
|
- /clearcache - Clear cache for current company
|
|
- /clearcache all - Clear entire cache (all companies)
|
|
|
|
Args:
|
|
update: Telegram update object
|
|
context: Telegram context
|
|
"""
|
|
try:
|
|
telegram_user_id = update.effective_user.id
|
|
logger.info(f"/clearcache command from user {telegram_user_id}")
|
|
|
|
# Check if user is linked
|
|
is_linked = await check_user_linked(telegram_user_id)
|
|
if not is_linked:
|
|
await update.message.reply_text(
|
|
"**Cont neconectat**\n\nFoloseste /start pentru a conecta contul.",
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
return
|
|
|
|
# Get auth data
|
|
auth_data = await get_user_auth_data(telegram_user_id)
|
|
jwt_token = auth_data['jwt_token']
|
|
|
|
# Check if user wants to clear all cache
|
|
clear_all = len(context.args) > 0 and context.args[0].lower() == 'all'
|
|
|
|
client = get_backend_client()
|
|
async with client:
|
|
if clear_all:
|
|
# Clear entire cache
|
|
result = await client.client.post(
|
|
"/api/cache/invalidate",
|
|
json={},
|
|
headers=client._get_auth_headers(jwt_token)
|
|
)
|
|
if result.status_code == 200:
|
|
await update.message.reply_text(
|
|
"✅ **Cache șters complet**\n\n"
|
|
"Toate datele cached au fost șterse.",
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
else:
|
|
await update.message.reply_text("❌ Eroare la ștergerea cache-ului.")
|
|
else:
|
|
# Get active company
|
|
session_manager = get_session_manager()
|
|
from 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
|
|
|
|
# Clear cache for current company
|
|
result = await client.client.post(
|
|
"/api/cache/invalidate",
|
|
json={"company_id": company['id']},
|
|
headers=client._get_auth_headers(jwt_token)
|
|
)
|
|
|
|
if result.status_code == 200:
|
|
await update.message.reply_text(
|
|
f"✅ **Cache șters pentru {company['name']}**\n\n"
|
|
"Datele vor fi reîncărcate la următoarea interogare.",
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
else:
|
|
await update.message.reply_text("❌ Eroare la ștergerea cache-ului.")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in clearcache_command: {e}", exc_info=True)
|
|
await update.message.reply_text("Eroare la ștergerea cache-ului.")
|
|
|
|
|
|
async def togglecache_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""
|
|
Handle /togglecache command.
|
|
|
|
Toggles cache on/off for the current user.
|
|
|
|
Args:
|
|
update: Telegram update object
|
|
context: Telegram context
|
|
"""
|
|
try:
|
|
telegram_user_id = update.effective_user.id
|
|
logger.info(f"/togglecache command from user {telegram_user_id}")
|
|
|
|
# Check if user is linked
|
|
is_linked = await check_user_linked(telegram_user_id)
|
|
if not is_linked:
|
|
await update.message.reply_text(
|
|
"**Cont neconectat**\n\nFoloseste /start pentru a conecta contul.",
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
return
|
|
|
|
# Get auth data
|
|
auth_data = await get_user_auth_data(telegram_user_id)
|
|
jwt_token = auth_data['jwt_token']
|
|
|
|
client = get_backend_client()
|
|
async with client:
|
|
# Get current cache stats to determine current state
|
|
stats_response = await client.client.get(
|
|
"/api/cache/stats",
|
|
headers=client._get_auth_headers(jwt_token)
|
|
)
|
|
|
|
if stats_response.status_code == 200:
|
|
stats = stats_response.json()
|
|
current_enabled = stats.get('user_cache_enabled', True)
|
|
|
|
# Toggle to opposite state
|
|
new_state = not current_enabled
|
|
|
|
toggle_response = await client.client.post(
|
|
"/api/cache/toggle-user",
|
|
json={"enabled": new_state},
|
|
headers=client._get_auth_headers(jwt_token)
|
|
)
|
|
|
|
if toggle_response.status_code == 200:
|
|
if new_state:
|
|
await update.message.reply_text(
|
|
"✅ **Cache activat**\n\n"
|
|
"Interogările tale vor folosi cache-ul pentru răspunsuri mai rapide.",
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
else:
|
|
await update.message.reply_text(
|
|
"⚠️ **Cache dezactivat**\n\n"
|
|
"Interogările tale vor accesa direct baza de date Oracle.",
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
else:
|
|
await update.message.reply_text("❌ Eroare la comutarea cache-ului.")
|
|
else:
|
|
await update.message.reply_text("❌ Eroare la citirea stării cache-ului.")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in togglecache_command: {e}", exc_info=True)
|
|
await update.message.reply_text("Eroare la comutarea cache-ului.")
|
|
|
|
|
|
async def companies_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""
|
|
Handle /companies command.
|
|
|
|
Shows list of companies the user has access to.
|
|
|
|
Args:
|
|
update: Telegram update object
|
|
context: Telegram context
|
|
"""
|
|
try:
|
|
telegram_user_id = update.effective_user.id
|
|
|
|
logger.info(f"/companies command from user {telegram_user_id}")
|
|
|
|
# Check if user is linked
|
|
is_linked = await check_user_linked(telegram_user_id)
|
|
|
|
if not is_linked:
|
|
await update.message.reply_text(
|
|
"**Cont neconectat**\n\n"
|
|
"Conecteaza-ti contul cu /start",
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
return
|
|
|
|
# Get companies
|
|
companies = await get_user_companies(telegram_user_id)
|
|
|
|
if not companies:
|
|
await update.message.reply_text(
|
|
"**Nicio companie gasita**\n\n"
|
|
"Contacteaza administratorul pentru permisiuni.",
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
return
|
|
|
|
# Format companies list
|
|
companies_text = f"**Companiile tale ({len(companies)}):**\n\n"
|
|
|
|
for i, comp in enumerate(companies, 1):
|
|
nume = comp.get('nume_firma', 'N/A')
|
|
comp_id = comp.get('id', 'N/A')
|
|
cui = comp.get('cui', 'N/A')
|
|
|
|
companies_text += f"{i}. **{nume}**\n"
|
|
companies_text += f" - ID: {comp_id}\n"
|
|
companies_text += f" - CUI: {cui}\n\n"
|
|
|
|
companies_text += "\nFoloseste /selectcompany pentru a selecta compania activa."
|
|
|
|
await update.message.reply_text(companies_text, parse_mode=ParseMode.MARKDOWN)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in companies_command: {e}", exc_info=True)
|
|
await update.message.reply_text(
|
|
"A aparut o eroare la incarcarea companiilor."
|
|
)
|
|
|
|
|
|
async def unlink_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""
|
|
Handle /unlink command.
|
|
|
|
Unlinks the user's Telegram account from Oracle account (security feature).
|
|
|
|
Args:
|
|
update: Telegram update object
|
|
context: Telegram context
|
|
"""
|
|
try:
|
|
telegram_user_id = update.effective_user.id
|
|
|
|
logger.info(f"/unlink command from user {telegram_user_id}")
|
|
|
|
# Check if linked
|
|
is_linked = await check_user_linked(telegram_user_id)
|
|
|
|
if not is_linked:
|
|
await update.message.reply_text(
|
|
"Contul tau nu este linkuit.",
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
return
|
|
|
|
# Create confirmation keyboard
|
|
keyboard = [
|
|
[
|
|
InlineKeyboardButton("Da, deconecteaza", callback_data="unlink_confirm"),
|
|
InlineKeyboardButton("Anuleaza", callback_data="unlink_cancel")
|
|
]
|
|
]
|
|
reply_markup = InlineKeyboardMarkup(keyboard)
|
|
|
|
await update.message.reply_text(
|
|
"**Confirmare Deconectare**\n\n"
|
|
"Esti sigur ca vrei sa deconectezi contul?\n\n"
|
|
"Accesul la date va fi oprit. Poti reconecta oricand cu un cod nou.\n\n"
|
|
"Confirma:",
|
|
reply_markup=reply_markup,
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in unlink_command: {e}", exc_info=True)
|
|
await update.message.reply_text(
|
|
"A aparut o eroare."
|
|
)
|
|
|
|
|
|
async def selectcompany_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""
|
|
Handle /selectcompany [search_term] command.
|
|
|
|
Permite căutare și selectare companie cu PAGINARE (identic cu butoanele).
|
|
|
|
Args:
|
|
update: Telegram update object
|
|
context: Telegram context
|
|
"""
|
|
try:
|
|
telegram_user_id = update.effective_user.id
|
|
|
|
logger.info(f"/selectcompany command from user {telegram_user_id}")
|
|
|
|
# Check if user is linked
|
|
is_linked = await check_user_linked(telegram_user_id)
|
|
if not is_linked:
|
|
await update.message.reply_text(
|
|
"**Cont neconectat**\n\nFoloseste /start",
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
return
|
|
|
|
# Get auth data
|
|
auth_data = await get_user_auth_data(telegram_user_id)
|
|
jwt_token = auth_data['jwt_token']
|
|
|
|
# Get search term from command arguments (optional)
|
|
search_term = " ".join(context.args) if context.args else ""
|
|
|
|
# ✅ MODIFICARE: Folosim funcția comună cu paginare
|
|
await _handle_selectcompany_view(
|
|
query_or_update=update,
|
|
telegram_user_id=telegram_user_id,
|
|
jwt_token=jwt_token,
|
|
is_callback=False,
|
|
page=0,
|
|
search_term=search_term
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in selectcompany_command: {e}", exc_info=True)
|
|
await update.message.reply_text("A aparut o eroare. Te rog incearca din nou.")
|
|
|
|
|
|
async def dashboard_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""Handle /dashboard command - shows financial dashboard."""
|
|
try:
|
|
telegram_user_id = update.effective_user.id
|
|
|
|
logger.info(f"/dashboard command from user {telegram_user_id}")
|
|
|
|
# Check linked
|
|
is_linked = await check_user_linked(telegram_user_id)
|
|
if not is_linked:
|
|
await update.message.reply_text(
|
|
"**Cont neconectat**\n\nFoloseste /start",
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
return
|
|
|
|
# Get active company
|
|
session_manager = get_session_manager()
|
|
from 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
|
|
|
|
# Extract cache metadata
|
|
cache_hit = treasury_data.get('cache_hit', False)
|
|
response_time_ms = treasury_data.get('response_time_ms', 0)
|
|
cache_source = treasury_data.get('cache_source', None)
|
|
|
|
# Format combined response (casa + banca) - rotunjit la leu (0 zecimale)
|
|
casa_total = round(treasury_data['casa']['total'])
|
|
banca_total = round(treasury_data['banca']['total'])
|
|
total_treasury = casa_total + banca_total
|
|
|
|
content = f"**Sold Total:** {total_treasury:,} RON\n\n"
|
|
content += f"**Defalcare:**\n"
|
|
content += f" - Casa: {casa_total:,} RON\n"
|
|
content += f" - Banca: {banca_total:,} RON\n\n"
|
|
content += "Foloseste butoanele pentru detalii:"
|
|
|
|
# Apply company header formatting
|
|
from app.bot.menus import format_response_with_company
|
|
text = format_response_with_company(content, company['name'])
|
|
|
|
# Add performance footer
|
|
if response_time_ms > 0:
|
|
from app.bot.formatters import add_performance_footer
|
|
text = add_performance_footer(text, cache_hit, response_time_ms, cache_source)
|
|
|
|
# Add buttons to view details
|
|
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
|
|
keyboard = InlineKeyboardMarkup([
|
|
[
|
|
InlineKeyboardButton("Detalii Casa", callback_data="menu:casa"),
|
|
InlineKeyboardButton("Detalii Banca", callback_data="menu:banca")
|
|
],
|
|
[
|
|
InlineKeyboardButton("Meniu Principal", callback_data="action:menu")
|
|
]
|
|
])
|
|
|
|
await update.message.reply_text(
|
|
text,
|
|
reply_markup=keyboard,
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in trezorerie_command: {e}", exc_info=True)
|
|
await update.message.reply_text("Eroare la incarcarea trezoreriei.")
|
|
|
|
|
|
async def menu_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""
|
|
Handle /menu command - shows main menu with interactive buttons.
|
|
|
|
Displays the main menu with 6 financial options organized in a 2-column layout.
|
|
Requires user to be linked and have an active company selected.
|
|
|
|
Args:
|
|
update: Telegram update object
|
|
context: Telegram context
|
|
"""
|
|
try:
|
|
telegram_user_id = update.effective_user.id
|
|
logger.info(f"/menu command from user {telegram_user_id}")
|
|
|
|
# Check if user is linked
|
|
is_linked = await check_user_linked(telegram_user_id)
|
|
if not is_linked:
|
|
await update.message.reply_text(
|
|
"**Cont neconectat**\n\nFoloseste /start",
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
return
|
|
|
|
# Get active company
|
|
session_manager = get_session_manager()
|
|
session = await session_manager.get_or_create_session(telegram_user_id)
|
|
company = session.get_active_company()
|
|
|
|
# Get company data for menu
|
|
company_name = company['name'] if company else None
|
|
company_cui = company.get('cui') if company else None
|
|
|
|
# Get cache status for user
|
|
cache_enabled = None
|
|
try:
|
|
from app.api.client import get_backend_client
|
|
client = get_backend_client()
|
|
async with client:
|
|
cache_stats = await client.get_cache_stats(jwt_token=auth_data['jwt_token'])
|
|
cache_enabled = cache_stats.get('user_enabled', True)
|
|
except Exception as e:
|
|
logger.warning(f"Could not get cache status: {e}")
|
|
|
|
# Create main menu (user is authenticated if they passed the is_linked check)
|
|
from app.bot.menus import create_main_menu, get_menu_message
|
|
keyboard = create_main_menu(company_name, company_cui, is_authenticated=True, cache_enabled=cache_enabled)
|
|
menu_text = get_menu_message(company_name, company_cui)
|
|
|
|
await update.message.reply_text(
|
|
menu_text,
|
|
reply_markup=keyboard,
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in menu_command: {e}", exc_info=True)
|
|
await update.message.reply_text("Eroare la afisarea meniului.")
|
|
|
|
|
|
async def trezorerie_casa_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""
|
|
Handle /trezorerie_casa command - shows cash treasury data.
|
|
|
|
Displays treasury data for cash accounts only (Casa).
|
|
|
|
Args:
|
|
update: Telegram update object
|
|
context: Telegram context
|
|
"""
|
|
try:
|
|
telegram_user_id = update.effective_user.id
|
|
logger.info(f"/trezorerie_casa command from user {telegram_user_id}")
|
|
|
|
# Check linked
|
|
is_linked = await check_user_linked(telegram_user_id)
|
|
if not is_linked:
|
|
await update.message.reply_text(
|
|
"**Cont neconectat**\n\nFoloseste /start",
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
return
|
|
|
|
# Get active company
|
|
session_manager = get_session_manager()
|
|
from 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, add_performance_footer
|
|
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'])
|
|
|
|
# Add performance footer if cache metadata is available
|
|
if 'cache_hit' in treasury_data and 'response_time_ms' in treasury_data:
|
|
cache_hit = treasury_data['cache_hit']
|
|
response_time_ms = treasury_data['response_time_ms']
|
|
cache_source = treasury_data.get('cache_source', None)
|
|
response = add_performance_footer(response, cache_hit, response_time_ms, cache_source)
|
|
|
|
keyboard = create_action_buttons("casa", show_export=True)
|
|
|
|
await update.message.reply_text(
|
|
response,
|
|
reply_markup=keyboard,
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in trezorerie_casa_command: {e}", exc_info=True)
|
|
await update.message.reply_text("Eroare la incarcarea trezoreriei cash.")
|
|
|
|
|
|
async def trezorerie_banca_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""
|
|
Handle /trezorerie_banca command - shows bank treasury data.
|
|
|
|
Displays treasury data for bank accounts only (Banca).
|
|
|
|
Args:
|
|
update: Telegram update object
|
|
context: Telegram context
|
|
"""
|
|
try:
|
|
telegram_user_id = update.effective_user.id
|
|
logger.info(f"/trezorerie_banca command from user {telegram_user_id}")
|
|
|
|
# Check linked
|
|
is_linked = await check_user_linked(telegram_user_id)
|
|
if not is_linked:
|
|
await update.message.reply_text(
|
|
"**Cont neconectat**\n\nFoloseste /start",
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
return
|
|
|
|
# Get active company
|
|
session_manager = get_session_manager()
|
|
from 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, add_performance_footer
|
|
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'])
|
|
|
|
# Add performance footer if cache metadata is available
|
|
if 'cache_hit' in treasury_data and 'response_time_ms' in treasury_data:
|
|
cache_hit = treasury_data['cache_hit']
|
|
response_time_ms = treasury_data['response_time_ms']
|
|
cache_source = treasury_data.get('cache_source', None)
|
|
response = add_performance_footer(response, cache_hit, response_time_ms, cache_source)
|
|
|
|
keyboard = create_action_buttons("banca", show_export=True)
|
|
|
|
await update.message.reply_text(
|
|
response,
|
|
reply_markup=keyboard,
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in trezorerie_banca_command: {e}", exc_info=True)
|
|
await update.message.reply_text("Eroare la incarcarea trezoreriei bancare.")
|
|
|
|
|
|
async def clienti_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""
|
|
Handle /clienti command - shows clients balance with maturity breakdown.
|
|
|
|
Displays total clients balance, in-term and overdue amounts, and list of clients
|
|
with interactive buttons to view details.
|
|
|
|
Args:
|
|
update: Telegram update object
|
|
context: Telegram context
|
|
"""
|
|
try:
|
|
telegram_user_id = update.effective_user.id
|
|
logger.info(f"/clienti command from user {telegram_user_id}")
|
|
|
|
# Check linked
|
|
is_linked = await check_user_linked(telegram_user_id)
|
|
if not is_linked:
|
|
await update.message.reply_text(
|
|
"**Cont neconectat**\n\nFoloseste /start",
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
return
|
|
|
|
# Get active company
|
|
session_manager = get_session_manager()
|
|
from 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
|
|
|
|
# Extract cache metadata
|
|
cache_hit = clients_data.get('cache_hit', False)
|
|
response_time_ms = clients_data.get('response_time_ms', 0)
|
|
cache_source = clients_data.get('cache_source', None)
|
|
|
|
# Format response
|
|
from app.bot.formatters import format_clients_balance_response, add_performance_footer
|
|
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'])
|
|
|
|
# Add performance footer
|
|
if response_time_ms > 0:
|
|
response = add_performance_footer(response, cache_hit, response_time_ms, cache_source)
|
|
|
|
keyboard = create_client_list_keyboard(clients_data['clients'], page=0)
|
|
|
|
await update.message.reply_text(
|
|
response,
|
|
reply_markup=keyboard,
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in clienti_command: {e}", exc_info=True)
|
|
await update.message.reply_text("Eroare la incarcarea datelor clienti.")
|
|
|
|
|
|
async def furnizori_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""
|
|
Handle /furnizori command - shows suppliers balance with maturity breakdown.
|
|
|
|
Displays total suppliers balance, in-term and overdue amounts, and list of suppliers
|
|
with interactive buttons to view details.
|
|
|
|
Args:
|
|
update: Telegram update object
|
|
context: Telegram context
|
|
"""
|
|
try:
|
|
telegram_user_id = update.effective_user.id
|
|
logger.info(f"/furnizori command from user {telegram_user_id}")
|
|
|
|
# Check linked
|
|
is_linked = await check_user_linked(telegram_user_id)
|
|
if not is_linked:
|
|
await update.message.reply_text(
|
|
"**Cont neconectat**\n\nFoloseste /start",
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
return
|
|
|
|
# Get active company
|
|
session_manager = get_session_manager()
|
|
from 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
|
|
|
|
# Extract cache metadata
|
|
cache_hit = suppliers_data.get('cache_hit', False)
|
|
response_time_ms = suppliers_data.get('response_time_ms', 0)
|
|
cache_source = suppliers_data.get('cache_source', None)
|
|
|
|
# Format response
|
|
from app.bot.formatters import format_suppliers_balance_response, add_performance_footer
|
|
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'])
|
|
|
|
# Add performance footer
|
|
if response_time_ms > 0:
|
|
response = add_performance_footer(response, cache_hit, response_time_ms, cache_source)
|
|
|
|
keyboard = create_supplier_list_keyboard(suppliers_data['suppliers'], page=0)
|
|
|
|
await update.message.reply_text(
|
|
response,
|
|
reply_markup=keyboard,
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in furnizori_command: {e}", exc_info=True)
|
|
await update.message.reply_text("Eroare la incarcarea datelor furnizori.")
|
|
|
|
|
|
async def evolutie_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""
|
|
Handle /evolutie command - shows cash flow evolution (collections/payments).
|
|
|
|
Displays performance data and monthly cash flow trends for collections and payments.
|
|
|
|
Args:
|
|
update: Telegram update object
|
|
context: Telegram context
|
|
"""
|
|
try:
|
|
telegram_user_id = update.effective_user.id
|
|
logger.info(f"/evolutie command from user {telegram_user_id}")
|
|
|
|
# Check linked
|
|
is_linked = await check_user_linked(telegram_user_id)
|
|
if not is_linked:
|
|
await update.message.reply_text(
|
|
"**Cont neconectat**\n\nFoloseste /start",
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
return
|
|
|
|
# Get active company
|
|
session_manager = get_session_manager()
|
|
from 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, add_performance_footer
|
|
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'])
|
|
|
|
# Add performance footer if cache metadata is available
|
|
if 'cache_hit' in evolution_data and 'response_time_ms' in evolution_data:
|
|
cache_hit = evolution_data['cache_hit']
|
|
response_time_ms = evolution_data['response_time_ms']
|
|
cache_source = evolution_data.get('cache_source', None)
|
|
response = add_performance_footer(response, cache_hit, response_time_ms, cache_source)
|
|
|
|
keyboard = create_action_buttons("evolutie", show_export=False, show_refresh=False)
|
|
|
|
await update.message.reply_text(
|
|
response,
|
|
reply_markup=keyboard,
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in evolutie_command: {e}", exc_info=True)
|
|
await update.message.reply_text("Eroare la incarcarea datelor evolutie.")
|
|
|
|
|
|
# ============================================================================
|
|
# TEXT MESSAGE HANDLERS
|
|
# ============================================================================
|
|
|
|
async def handle_text_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|
"""
|
|
Handle regular text messages.
|
|
|
|
Automatically detects and processes linking codes when user sends
|
|
a text that matches the code format (8 alphanumeric characters).
|
|
|
|
Args:
|
|
update: Telegram update object
|
|
context: Telegram context
|
|
"""
|
|
try:
|
|
telegram_user = update.effective_user
|
|
telegram_user_id = telegram_user.id
|
|
text = update.message.text.strip().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, add_performance_footer
|
|
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'])
|
|
|
|
# Add performance footer if cache metadata is available
|
|
if 'cache_hit' in treasury_data and 'response_time_ms' in treasury_data:
|
|
cache_hit = treasury_data['cache_hit']
|
|
response_time_ms = treasury_data['response_time_ms']
|
|
cache_source = treasury_data.get('cache_source', None)
|
|
response = add_performance_footer(response, cache_hit, response_time_ms, cache_source)
|
|
|
|
keyboard = create_action_buttons("casa", show_export=False, show_refresh=False)
|
|
|
|
try:
|
|
await query.edit_message_text(
|
|
response,
|
|
reply_markup=keyboard,
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
except Exception as e:
|
|
# Ignore "Message is not modified" error
|
|
if "Message is not modified" not in str(e):
|
|
raise
|
|
|
|
elif action == "banca":
|
|
# Trezorerie banca
|
|
from 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, add_performance_footer
|
|
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'])
|
|
|
|
# Add performance footer if cache metadata is available
|
|
if 'cache_hit' in treasury_data and 'response_time_ms' in treasury_data:
|
|
cache_hit = treasury_data['cache_hit']
|
|
response_time_ms = treasury_data['response_time_ms']
|
|
cache_source = treasury_data.get('cache_source', None)
|
|
response = add_performance_footer(response, cache_hit, response_time_ms, cache_source)
|
|
|
|
keyboard = create_action_buttons("banca", show_export=False, show_refresh=False)
|
|
|
|
try:
|
|
await query.edit_message_text(
|
|
response,
|
|
reply_markup=keyboard,
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
except Exception as e:
|
|
# Ignore "Message is not modified" error
|
|
if "Message is not modified" not in str(e):
|
|
raise
|
|
|
|
elif action == "clienti":
|
|
# Sold clienți + listă cu paginare
|
|
from 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, add_performance_footer
|
|
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'])
|
|
|
|
# Add performance footer if cache metadata is available
|
|
if 'cache_hit' in clients_data and 'response_time_ms' in clients_data:
|
|
cache_hit = clients_data['cache_hit']
|
|
response_time_ms = clients_data['response_time_ms']
|
|
cache_source = clients_data.get('cache_source', None)
|
|
response = add_performance_footer(response, cache_hit, response_time_ms, cache_source)
|
|
|
|
keyboard = create_client_list_keyboard(clients_data['clients'], page=0)
|
|
|
|
await query.edit_message_text(
|
|
response,
|
|
reply_markup=keyboard,
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
|
|
elif action == "furnizori":
|
|
# Sold furnizori + listă cu paginare
|
|
from 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, add_performance_footer
|
|
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'])
|
|
|
|
# Add performance footer if cache metadata is available
|
|
if 'cache_hit' in suppliers_data and 'response_time_ms' in suppliers_data:
|
|
cache_hit = suppliers_data['cache_hit']
|
|
response_time_ms = suppliers_data['response_time_ms']
|
|
cache_source = suppliers_data.get('cache_source', None)
|
|
response = add_performance_footer(response, cache_hit, response_time_ms, cache_source)
|
|
|
|
keyboard = create_supplier_list_keyboard(suppliers_data['suppliers'], page=0)
|
|
|
|
await query.edit_message_text(
|
|
response,
|
|
reply_markup=keyboard,
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
|
|
elif action == "evolutie":
|
|
# Evoluție cash flow
|
|
from 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, add_performance_footer
|
|
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'])
|
|
|
|
# Add performance footer if cache metadata is available
|
|
if 'cache_hit' in evolution_data and 'response_time_ms' in evolution_data:
|
|
cache_hit = evolution_data['cache_hit']
|
|
response_time_ms = evolution_data['response_time_ms']
|
|
cache_source = evolution_data.get('cache_source', None)
|
|
response = add_performance_footer(response, cache_hit, response_time_ms, cache_source)
|
|
|
|
keyboard = create_action_buttons("evolutie", show_export=False, show_refresh=False)
|
|
|
|
await query.edit_message_text(
|
|
response,
|
|
reply_markup=keyboard,
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
|
|
elif action == "togglecache":
|
|
# Toggle cache pentru user
|
|
try:
|
|
client = get_backend_client()
|
|
async with client:
|
|
cache_stats = await client.get_cache_stats(jwt_token=jwt_token)
|
|
user_enabled = cache_stats.get('user_enabled', True)
|
|
|
|
# Create toggle buttons
|
|
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
|
|
keyboard = [
|
|
[
|
|
InlineKeyboardButton(
|
|
"✅ Activează" if not user_enabled else "❌ Dezactivează",
|
|
callback_data=f"cache_toggle:{'on' if not user_enabled else 'off'}"
|
|
)
|
|
],
|
|
[InlineKeyboardButton("« Înapoi la Meniu", callback_data="action:menu")]
|
|
]
|
|
reply_markup = InlineKeyboardMarkup(keyboard)
|
|
|
|
status = "ACTIVAT" if user_enabled else "DEZACTIVAT"
|
|
message = f"**Cache Status**\n\nCurent: {status}\n\n"
|
|
|
|
if user_enabled:
|
|
message += "Vrei să dezactivezi cache-ul temporar?\nFolosește pentru teste de performanță."
|
|
else:
|
|
message += "Cache-ul este dezactivat.\nToate queries merg direct la Oracle."
|
|
|
|
await query.edit_message_text(
|
|
message,
|
|
reply_markup=reply_markup,
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Toggle cache menu error: {e}", exc_info=True)
|
|
await query.answer("Eroare la obținerea status cache.", show_alert=True)
|
|
|
|
elif action == "clearcache":
|
|
# Clear cache
|
|
try:
|
|
# Create inline keyboard
|
|
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
|
|
keyboard = [
|
|
[
|
|
InlineKeyboardButton("Toate companiile", callback_data="cache_clear:all"),
|
|
InlineKeyboardButton("Doar compania mea", callback_data="cache_clear:current")
|
|
],
|
|
[InlineKeyboardButton("« Înapoi la Meniu", callback_data="action:menu")]
|
|
]
|
|
reply_markup = InlineKeyboardMarkup(keyboard)
|
|
|
|
message = "**🔄 Invalidare Cache**\n\n"
|
|
if company:
|
|
message += f"Compania curentă: {company['name']}\n\n"
|
|
message += "Alege scope:"
|
|
|
|
await query.edit_message_text(
|
|
message,
|
|
reply_markup=reply_markup,
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Clear cache menu error: {e}", exc_info=True)
|
|
await query.answer("Eroare la afișarea opțiuni cache.", show_alert=True)
|
|
|
|
elif action == "select_company":
|
|
# ✅ MODIFICARE: Folosim funcția comună
|
|
await _handle_selectcompany_view(
|
|
query_or_update=query,
|
|
telegram_user_id=telegram_user_id,
|
|
jwt_token=jwt_token,
|
|
is_callback=True,
|
|
page=0,
|
|
search_term=""
|
|
)
|
|
|
|
|
|
async def handle_action_callback(query, telegram_user_id: int, callback_data: str):
|
|
"""
|
|
Handle action button clicks (Refresh, Export, Menu, Help, Login, Logout).
|
|
|
|
Callback format: action:{type}:{view}
|
|
Types: refresh, export, menu, help, login, logout
|
|
|
|
Args:
|
|
query: CallbackQuery object
|
|
telegram_user_id: Telegram user ID
|
|
callback_data: Callback data string
|
|
"""
|
|
parts = callback_data.split(":")
|
|
action_type = parts[1]
|
|
|
|
if action_type == "menu":
|
|
# Back to main menu
|
|
session_manager = get_session_manager()
|
|
session = await session_manager.get_or_create_session(telegram_user_id)
|
|
company = session.get_active_company()
|
|
|
|
# Check if user is authenticated
|
|
auth_data = await get_user_auth_data(telegram_user_id)
|
|
is_authenticated = auth_data is not None
|
|
|
|
# Get cache status for user
|
|
cache_enabled = None
|
|
if is_authenticated:
|
|
try:
|
|
from app.api.client import get_backend_client
|
|
client = get_backend_client()
|
|
async with client:
|
|
cache_stats = await client.get_cache_stats(jwt_token=auth_data['jwt_token'])
|
|
cache_enabled = cache_stats.get('user_enabled', True)
|
|
except Exception as e:
|
|
logger.warning(f"Could not get cache status: {e}")
|
|
|
|
from 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, cache_enabled)
|
|
menu_text = get_menu_message(company_name, company_cui)
|
|
|
|
await query.edit_message_text(
|
|
menu_text,
|
|
reply_markup=keyboard,
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
|
|
elif action_type == "refresh":
|
|
# Refresh current view
|
|
view = parts[2] if len(parts) > 2 else "sold"
|
|
|
|
# Check if it's a detail view (client_detail:name or supplier_detail:name)
|
|
if view.startswith("client_detail:"):
|
|
entity_name = view.split(":", 1)[1] # Extract entity name
|
|
# Limit name to 40 chars for Telegram callback_data limit (64 bytes)
|
|
safe_name = entity_name[:40] if len(entity_name) > 40 else entity_name
|
|
await handle_details_callback(query, telegram_user_id, f"details:client:{safe_name}:0")
|
|
elif view.startswith("supplier_detail:"):
|
|
entity_name = view.split(":", 1)[1] # Extract entity name
|
|
# Limit name to 40 chars for Telegram callback_data limit (64 bytes)
|
|
safe_name = entity_name[:40] if len(entity_name) > 40 else entity_name
|
|
await handle_details_callback(query, telegram_user_id, f"details:supplier:{safe_name}:0")
|
|
else:
|
|
# Regular menu view refresh
|
|
await handle_menu_callback(query, telegram_user_id, f"menu:{view}")
|
|
|
|
elif action_type == "export":
|
|
# Export functionality (placeholder for now)
|
|
await query.answer("Functia de export va fi disponibila in curand", show_alert=True)
|
|
|
|
elif action_type == "help":
|
|
# Show help message above menu (edit current message)
|
|
from 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)
|
|
|
|
# ========== CACHE CALLBACKS (FAZA 6) ==========
|
|
|
|
elif callback_data.startswith("cache_toggle:"):
|
|
# Handle cache toggle button
|
|
action = callback_data.split(":")[1]
|
|
enabled = action == "on"
|
|
|
|
auth_data = await get_user_auth_data(telegram_user_id)
|
|
jwt_token = auth_data['jwt_token']
|
|
|
|
try:
|
|
client = get_backend_client()
|
|
async with client:
|
|
await client.toggle_user_cache(jwt_token=jwt_token, enabled=enabled)
|
|
|
|
status = "activat" if enabled else "dezactivat"
|
|
message = f"✅ **Cache {status}** pentru tine.\n\n"
|
|
|
|
if enabled:
|
|
message += "Queries vor fi servite din cache când e posibil."
|
|
else:
|
|
message += "Toate queries vor merge direct la Oracle.\nFolosește /togglecache din nou pentru reactivare."
|
|
|
|
# Add back button
|
|
keyboard = InlineKeyboardMarkup([
|
|
[InlineKeyboardButton("« Înapoi la Meniu", callback_data="action:menu")]
|
|
])
|
|
|
|
await query.edit_message_text(
|
|
message,
|
|
reply_markup=keyboard,
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Toggle cache callback error: {e}", exc_info=True)
|
|
await query.answer("❌ Eroare la modificarea setării cache.", show_alert=True)
|
|
|
|
elif callback_data.startswith("cache_clear:"):
|
|
# Handle clear cache button
|
|
scope = callback_data.split(":")[1]
|
|
|
|
auth_data = await get_user_auth_data(telegram_user_id)
|
|
jwt_token = auth_data['jwt_token']
|
|
|
|
try:
|
|
client = get_backend_client()
|
|
|
|
if scope == "all":
|
|
# Clear all cache
|
|
async with client:
|
|
await client.invalidate_cache(jwt_token=jwt_token, company_id=None)
|
|
message = "✅ Cache invalidat pentru **toate companiile**.\n\nDatele vor fi refreshate la următoarea interogare."
|
|
|
|
elif scope == "current":
|
|
# Clear only current company
|
|
session_manager = get_session_manager()
|
|
session = await session_manager.get_or_create_session(telegram_user_id)
|
|
company = session.get_active_company()
|
|
|
|
if not company:
|
|
await query.answer("Nu ai o companie selectată.", show_alert=True)
|
|
return
|
|
|
|
async with client:
|
|
await client.invalidate_cache(jwt_token=jwt_token, company_id=company['id'])
|
|
message = f"✅ Cache invalidat pentru **{company['name']}**.\n\nDatele vor fi refreshate la următoarea interogare."
|
|
|
|
# Add back button
|
|
keyboard = InlineKeyboardMarkup([
|
|
[InlineKeyboardButton("« Înapoi la Meniu", callback_data="action:menu")]
|
|
])
|
|
|
|
await query.edit_message_text(
|
|
message,
|
|
reply_markup=keyboard,
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Clear cache callback error: {e}", exc_info=True)
|
|
await query.answer("❌ Eroare la ștergerea cache-ului.", show_alert=True)
|
|
|
|
# ========== PAGINATION CALLBACKS ==========
|
|
|
|
elif callback_data.startswith("clients_page:"):
|
|
# Handle clients pagination
|
|
page = int(callback_data.split(":")[1])
|
|
|
|
# Get auth data and company
|
|
auth_data = await get_user_auth_data(telegram_user_id)
|
|
jwt_token = auth_data['jwt_token']
|
|
|
|
session_manager = get_session_manager()
|
|
session = await session_manager.get_or_create_session(telegram_user_id)
|
|
company = session.get_active_company()
|
|
|
|
# Get clients with maturity
|
|
from 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, add_performance_footer
|
|
from app.bot.menus import create_action_buttons, format_response_with_company
|
|
|
|
# Extract cache metadata
|
|
cache_hit = data.get('cache_hit', False)
|
|
response_time_ms = data.get('response_time_ms', 0)
|
|
cache_source = data.get('cache_source', None)
|
|
|
|
content = format_dashboard_response(data)
|
|
response = format_response_with_company(content, company['name'])
|
|
|
|
# Add performance footer
|
|
if response_time_ms > 0:
|
|
response = add_performance_footer(response, cache_hit, response_time_ms, cache_source)
|
|
keyboard = create_action_buttons("sold", show_export=False, show_refresh=False)
|
|
|
|
if is_callback:
|
|
await query_or_update.edit_message_text(
|
|
response,
|
|
reply_markup=keyboard,
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
else:
|
|
await query_or_update.message.reply_text(
|
|
response,
|
|
reply_markup=keyboard,
|
|
parse_mode=ParseMode.MARKDOWN
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in _handle_sold_view: {e}", exc_info=True)
|
|
error_msg = "Eroare la incarcarea dashboard-ului."
|
|
if is_callback:
|
|
await query_or_update.answer(error_msg, show_alert=True)
|
|
else:
|
|
await query_or_update.message.reply_text(error_msg)
|
|
|
|
|
|
async def _handle_selectcompany_view(
|
|
query_or_update,
|
|
telegram_user_id: int,
|
|
jwt_token: str,
|
|
is_callback: bool = False,
|
|
page: int = 0,
|
|
search_term: str = ""
|
|
):
|
|
"""
|
|
Common handler pentru company selection cu paginare.
|
|
|
|
Folosit de:
|
|
- Comanda /selectcompany
|
|
- Butonul menu:select_company
|
|
- Callback-urile de paginare (select_company_page:N)
|
|
|
|
Args:
|
|
query_or_update: Query (callback) sau Update (command)
|
|
telegram_user_id: Telegram user ID
|
|
jwt_token: JWT token
|
|
is_callback: True dacă e apelat din callback, False dacă e command
|
|
page: Numărul paginii (0-indexed)
|
|
search_term: Termen de căutare (opțional)
|
|
"""
|
|
try:
|
|
client = get_backend_client()
|
|
async with client:
|
|
companies = await client.get_user_companies(jwt_token=jwt_token)
|
|
|
|
# Apply search filter if provided
|
|
if search_term:
|
|
from 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'
|
|
]
|