Add cache source tracking (L1/L2) for Telegram bot responses

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>
This commit is contained in:
2025-11-07 22:39:09 +02:00
parent 87bd04e3ff
commit 2a37959d80
5 changed files with 759 additions and 60 deletions

View File

@@ -51,6 +51,11 @@ class TrendsResponse(BaseModel):
metadata: Dict[str, Any]
growth_rates: Optional[Dict[str, float]] = None
# Cache metadata (optional, for Telegram Bot)
cache_hit: Optional[bool] = None
response_time_ms: Optional[float] = None
cache_source: Optional[str] = None
class DashboardSummary(BaseModel):
"""Model pentru toate datele dashboard-ului"""
# CLIENȚI - statistici existente

View File

@@ -1,4 +1,4 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from typing import Optional
import sys
import os
@@ -14,25 +14,42 @@ from ..services.dashboard_service import DashboardService
router = APIRouter()
@router.get("/summary", response_model=DashboardSummary)
@router.get("/summary")
async def get_dashboard_summary(
request: Request,
company: str = Query(description="Codul firmei"),
current_user: CurrentUser = Depends(get_current_user)
):
"""
Obține toate datele pentru dashboard într-un singur apel
- Necesită autentificare JWT
- Returnează statistici clienți/furnizori și trezorerie
- Include metadata cache pentru Telegram Bot (X-Include-Cache-Metadata header)
"""
try:
# Verifică dacă utilizatorul are acces la firma specificată
if company not in current_user.companies:
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
result = await DashboardService.get_complete_summary(company, current_user.username)
return result
result = await DashboardService.get_complete_summary(company, current_user.username, request=request)
# Convert Pydantic model to dict for JSON serialization
result_dict = result.dict() if hasattr(result, 'dict') else result
# Add cache metadata if requested (for Telegram Bot)
include_metadata = request.headers.get('X-Include-Cache-Metadata', '').lower() == 'true'
if include_metadata:
cache_hit = getattr(request.state, 'cache_hit', False)
response_time = getattr(request.state, 'response_time_ms', 0)
cache_source = getattr(request.state, 'cache_source', None)
result_dict['cache_hit'] = cache_hit
result_dict['response_time_ms'] = response_time
# Always include cache_source, even if None
result_dict['cache_source'] = cache_source
return result_dict
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
@@ -40,6 +57,7 @@ async def get_dashboard_summary(
@router.get("/trends", response_model=TrendsResponse)
async def get_dashboard_trends(
request: Request,
company: str = Query(description="Codul firmei"),
period: str = Query(default="30d", description="Perioada pentru trends: 7d, 30d, ytd, 12m"),
compare_previous: bool = Query(default=True, description="Compară cu perioada anterioară"),
@@ -47,7 +65,7 @@ async def get_dashboard_trends(
):
"""
Obține trenduri pentru indicatorii principali (clienți/furnizori)
- period: "7d" (7 zile), "30d" (30 zile), "ytd" (year to date), "12m" (12 luni)
- compare_previous: dacă să compare cu perioada anterioară
- Necesită autentificare JWT
@@ -57,21 +75,34 @@ async def get_dashboard_trends(
# Verifică dacă utilizatorul are acces la firma specificată
if company not in current_user.companies:
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
# Validează perioada
valid_periods = ["7d", "30d", "ytd", "12m"]
if period not in valid_periods:
raise HTTPException(
status_code=400,
status_code=400,
detail=f"Perioadă nevalidă: {period}. Valori permise: {', '.join(valid_periods)}"
)
# Obține datele de trenduri
result = await DashboardService.get_trends(int(company), period)
# The service now returns the data in the correct format
# Return it directly as TrendsResponse
return TrendsResponse(**result)
result = await DashboardService.get_trends(int(company), period, request=request)
# Convert to dict if needed
result_dict = result.dict() if hasattr(result, 'dict') else result
# Add cache metadata if requested (for Telegram Bot)
include_metadata = request.headers.get('X-Include-Cache-Metadata', '').lower() == 'true'
if include_metadata:
cache_hit = getattr(request.state, 'cache_hit', False)
response_time = getattr(request.state, 'response_time_ms', 0)
cache_source = getattr(request.state, 'cache_source', None)
result_dict['cache_hit'] = cache_hit
result_dict['response_time_ms'] = response_time
# Always include cache_source, even if None
result_dict['cache_source'] = cache_source
# Return as TrendsResponse
return TrendsResponse(**result_dict)
except ValueError as e:
logger.error(f"Value error in trends endpoint: {str(e)}")
@@ -191,6 +222,7 @@ async def get_cashflow(
@router.get("/maturity")
async def get_maturity_analysis(
request: Request,
company: int = Query(..., description="ID-ul firmei"),
period: str = Query("7d", regex="^(7d|1m|3m|6m|12m|all)$", description="Orizont de planificare pentru analiza scadențelor"),
current_user: CurrentUser = Depends(get_current_user)
@@ -210,15 +242,31 @@ async def get_maturity_analysis(
- Compară scadențele clienți vs furnizori
- Calculează balanța și oferă recomandări
- Returnează metadate cu statistici complete
- Include metadata cache pentru Telegram Bot (X-Include-Cache-Metadata header)
"""
try:
# Verifică dacă utilizatorul are acces la firma specificată
if str(company) not in current_user.companies:
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
result = await DashboardService.get_maturity_analysis(company, period)
return result
result = await DashboardService.get_maturity_analysis(company, period, request=request)
# Convert to dict if needed
result_dict = result.dict() if hasattr(result, 'dict') else result
# Add cache metadata if requested (for Telegram Bot)
include_metadata = request.headers.get('X-Include-Cache-Metadata', '').lower() == 'true'
if include_metadata:
cache_hit = getattr(request.state, 'cache_hit', False)
response_time = getattr(request.state, 'response_time_ms', 0)
cache_source = getattr(request.state, 'cache_source', None)
result_dict['cache_hit'] = cache_hit
result_dict['response_time_ms'] = response_time
# Always include cache_source, even if None
result_dict['cache_source'] = cache_source
return result_dict
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
@@ -252,23 +300,40 @@ async def get_monthly_flows(
@router.get("/treasury-breakdown")
async def get_treasury_breakdown(
request: Request,
company: int = Query(..., description="ID-ul firmei"),
current_user: CurrentUser = Depends(get_current_user)
):
"""
Returnează defalcarea trezoreriei pentru firma selectată
- Necesită autentificare JWT
- Returnează distribuția soldurilor pe conturi și tipuri
- Include metadata cache pentru Telegram Bot (X-Include-Cache-Metadata header)
"""
try:
# Verifică dacă utilizatorul are acces la firma specificată
if str(company) not in current_user.companies:
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
result = await DashboardService.get_treasury_breakdown(company)
return result
result = await DashboardService.get_treasury_breakdown(company, request=request)
# Convert to dict if needed
result_dict = result.dict() if hasattr(result, 'dict') else result
# Add cache metadata if requested (for Telegram Bot)
include_metadata = request.headers.get('X-Include-Cache-Metadata', '').lower() == 'true'
if include_metadata:
cache_hit = getattr(request.state, 'cache_hit', False)
response_time = getattr(request.state, 'response_time_ms', 0)
cache_source = getattr(request.state, 'cache_source', None)
result_dict['cache_hit'] = cache_hit
result_dict['response_time_ms'] = response_time
# Always include cache_source, even if None
result_dict['cache_source'] = cache_source
return result_dict
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
@@ -277,22 +342,39 @@ async def get_treasury_breakdown(
@router.get("/net-balance-breakdown")
async def get_net_balance_breakdown(
request: Request,
company: int = Query(..., description="ID-ul firmei"),
current_user: CurrentUser = Depends(get_current_user)
):
"""
Returnează defalcarea balanței nete pentru firma selectată
- Necesită autentificare JWT
- Returnează analiza detaliată a balanței nete
- Include metadata cache pentru Telegram Bot (X-Include-Cache-Metadata header)
"""
try:
# Verifică dacă utilizatorul are acces la firma specificată
if str(company) not in current_user.companies:
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
result = await DashboardService.get_net_balance_breakdown(company)
return result
result = await DashboardService.get_net_balance_breakdown(company, request=request)
# Convert to dict if needed
result_dict = result.dict() if hasattr(result, 'dict') else result
# Add cache metadata if requested (for Telegram Bot)
include_metadata = request.headers.get('X-Include-Cache-Metadata', '').lower() == 'true'
if include_metadata:
cache_hit = getattr(request.state, 'cache_hit', False)
response_time = getattr(request.state, 'response_time_ms', 0)
cache_source = getattr(request.state, 'cache_source', None)
result_dict['cache_hit'] = cache_hit
result_dict['response_time_ms'] = response_time
# Always include cache_source, even if None
result_dict['cache_source'] = cache_source
return result_dict
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))

View File

@@ -234,15 +234,20 @@ class BackendAPIClient:
Returns:
Dict with dashboard data (sold_total, facturi, plati, etc.)
Includes _cache_hit and _response_time_ms metadata
"""
try:
if not self.client:
self.client = AsyncClient(base_url=self.base_url, timeout=REQUEST_TIMEOUT)
# Add cache metadata header for Telegram Bot
headers = self._get_auth_headers(jwt_token)
headers['X-Include-Cache-Metadata'] = 'true'
response = await self.client.get(
"/api/dashboard/summary",
params={"company": str(company_id)},
headers=self._get_auth_headers(jwt_token)
headers=headers
)
return await self._handle_response(response)
@@ -270,9 +275,13 @@ class BackendAPIClient:
if not self.client:
self.client = AsyncClient(base_url=self.base_url, timeout=REQUEST_TIMEOUT)
# Add cache metadata header for Telegram Bot
headers = self._get_auth_headers(jwt_token)
headers['X-Include-Cache-Metadata'] = 'true'
response = await self.client.get(
f"/api/dashboard/treasury-breakdown?company={company_id}",
headers=self._get_auth_headers(jwt_token)
headers=headers
)
return await self._handle_response(response)
@@ -302,9 +311,13 @@ class BackendAPIClient:
if not self.client:
self.client = AsyncClient(base_url=self.base_url, timeout=REQUEST_TIMEOUT)
# Add cache metadata header for Telegram Bot
headers = self._get_auth_headers(jwt_token)
headers['X-Include-Cache-Metadata'] = 'true'
response = await self.client.get(
f"/api/dashboard/detailed-data?company={company_id}&data_type={data_type}",
headers=self._get_auth_headers(jwt_token)
headers=headers
)
return await self._handle_response(response)
@@ -334,9 +347,13 @@ class BackendAPIClient:
if not self.client:
self.client = AsyncClient(base_url=self.base_url, timeout=REQUEST_TIMEOUT)
# Add cache metadata header for Telegram Bot
headers = self._get_auth_headers(jwt_token)
headers['X-Include-Cache-Metadata'] = 'true'
response = await self.client.get(
f"/api/dashboard/maturity?company={company_id}&period={period}",
headers=self._get_auth_headers(jwt_token)
headers=headers
)
return await self._handle_response(response)
@@ -364,9 +381,13 @@ class BackendAPIClient:
if not self.client:
self.client = AsyncClient(base_url=self.base_url, timeout=REQUEST_TIMEOUT)
# Add cache metadata header for Telegram Bot
headers = self._get_auth_headers(jwt_token)
headers['X-Include-Cache-Metadata'] = 'true'
response = await self.client.get(
f"/api/dashboard/performance?company={company_id}",
headers=self._get_auth_headers(jwt_token)
headers=headers
)
return await self._handle_response(response)
@@ -396,9 +417,13 @@ class BackendAPIClient:
if not self.client:
self.client = AsyncClient(base_url=self.base_url, timeout=REQUEST_TIMEOUT)
# Add cache metadata header for Telegram Bot
headers = self._get_auth_headers(jwt_token)
headers['X-Include-Cache-Metadata'] = 'true'
response = await self.client.get(
f"/api/dashboard/monthly-flows?company={company_id}&months={months}",
headers=self._get_auth_headers(jwt_token)
headers=headers
)
return await self._handle_response(response)
@@ -428,9 +453,13 @@ class BackendAPIClient:
if not self.client:
self.client = AsyncClient(base_url=self.base_url, timeout=REQUEST_TIMEOUT)
# Add cache metadata header for Telegram Bot
headers = self._get_auth_headers(jwt_token)
headers['X-Include-Cache-Metadata'] = 'true'
response = await self.client.get(
f"/api/dashboard/trends?company={company_id}&period={period}",
headers=self._get_auth_headers(jwt_token)
headers=headers
)
return await self._handle_response(response)
@@ -476,9 +505,6 @@ class BackendAPIClient:
if filters:
params.update(filters)
# ⚠️ DEBUGGING: Log exact parameters being sent
logger.info(f"📤 Searching invoices with params: {params}")
response = await self.client.get(
"/api/invoices/",
params=params,
@@ -487,13 +513,10 @@ class BackendAPIClient:
data = await self._handle_response(response)
# ⚠️ DEBUGGING: Log response
if isinstance(data, dict) and 'invoices' in data:
invoice_list = data['invoices']
logger.info(f"📥 Received {len(invoice_list)} invoices from backend")
return invoice_list
elif isinstance(data, list):
logger.info(f"📥 Received {len(data)} invoices from backend (direct list)")
return data
else:
logger.warning(f"📥 Unexpected response format: {type(data)}")
@@ -626,6 +649,113 @@ class BackendAPIClient:
logger.error(f"Failed to export report: {e}")
return None
# =========================================================================
# CACHE MANAGEMENT
# =========================================================================
async def invalidate_cache(
self,
jwt_token: str,
company_id: Optional[int] = None,
cache_type: Optional[str] = None
) -> bool:
"""
Invalidate cache entries.
Args:
jwt_token: JWT access token
company_id: Optional company ID (None = all companies)
cache_type: Optional cache type (None = all types)
Returns:
bool: True if successful
"""
try:
if not self.client:
self.client = AsyncClient(base_url=self.base_url, timeout=REQUEST_TIMEOUT)
request_data = {}
if company_id is not None:
request_data['company_id'] = company_id
if cache_type is not None:
request_data['cache_type'] = cache_type
response = await self.client.post(
"/api/cache/invalidate",
json=request_data,
headers=self._get_auth_headers(jwt_token)
)
response.raise_for_status()
logger.info(f"Cache invalidated: company_id={company_id}, cache_type={cache_type}")
return True
except Exception as e:
logger.error(f"Failed to invalidate cache: {e}")
return False
async def toggle_user_cache(
self,
jwt_token: str,
enabled: bool
) -> bool:
"""
Toggle cache for current user.
Args:
jwt_token: JWT access token
enabled: True to enable cache, False to disable
Returns:
bool: True if successful
"""
try:
if not self.client:
self.client = AsyncClient(base_url=self.base_url, timeout=REQUEST_TIMEOUT)
response = await self.client.post(
"/api/cache/toggle-user",
json={"enabled": enabled},
headers=self._get_auth_headers(jwt_token)
)
response.raise_for_status()
logger.info(f"User cache toggled: enabled={enabled}")
return True
except Exception as e:
logger.error(f"Failed to toggle user cache: {e}")
return False
async def get_cache_stats(
self,
jwt_token: str
) -> Optional[Dict[str, Any]]:
"""
Get cache statistics including user-specific settings.
Args:
jwt_token: JWT access token
Returns:
Dict with cache stats including 'user_enabled' field
"""
try:
if not self.client:
self.client = AsyncClient(base_url=self.base_url, timeout=REQUEST_TIMEOUT)
response = await self.client.get(
"/api/cache/stats",
headers=self._get_auth_headers(jwt_token)
)
response.raise_for_status()
return response.json()
except Exception as e:
logger.error(f"Failed to get cache stats: {e}")
return None
# =========================================================================
# HEALTH CHECK
# =========================================================================

View File

@@ -69,6 +69,7 @@ async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
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()
@@ -77,8 +78,19 @@ async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
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)
keyboard = create_main_menu(company_name, company_cui, is_authenticated=True, cache_enabled=cache_enabled)
# Single welcome message with menu
if company_name:
@@ -209,6 +221,11 @@ Dupa conectarea contului, foloseste **butoanele interactive** pentru:
/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
@@ -263,6 +280,158 @@ async def clear_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
)
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.
@@ -574,6 +743,11 @@ async def trezorerie_command(update: Update, context: ContextTypes.DEFAULT_TYPE)
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'])
@@ -589,6 +763,11 @@ async def trezorerie_command(update: Update, context: ContextTypes.DEFAULT_TYPE)
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([
@@ -645,9 +824,20 @@ async def menu_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
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)
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(
@@ -708,11 +898,19 @@ async def trezorerie_casa_command(update: Update, context: ContextTypes.DEFAULT_
return
# Format response
from app.bot.formatters import format_treasury_casa_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(
@@ -773,11 +971,19 @@ async def trezorerie_banca_command(update: Update, context: ContextTypes.DEFAULT
return
# Format response
from app.bot.formatters import format_treasury_banca_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(
@@ -838,8 +1044,13 @@ async def clienti_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
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
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(
@@ -847,6 +1058,11 @@ async def clienti_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
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(
@@ -907,8 +1123,13 @@ async def furnizori_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
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
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(
@@ -916,6 +1137,11 @@ async def furnizori_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
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(
@@ -976,7 +1202,7 @@ async def evolutie_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
return
# Format response
from app.bot.formatters import format_cashflow_evolution_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(
@@ -984,6 +1210,14 @@ async def evolutie_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
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(
@@ -1205,11 +1439,19 @@ async def handle_menu_callback(query, telegram_user_id: int, callback_data: str)
from app.bot.helpers import get_treasury_breakdown_split
treasury_data = await get_treasury_breakdown_split(company['id'], jwt_token)
from app.bot.formatters import format_treasury_casa_response
from app.bot.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:
@@ -1228,11 +1470,19 @@ async def handle_menu_callback(query, telegram_user_id: int, callback_data: str)
from app.bot.helpers import get_treasury_breakdown_split
treasury_data = await get_treasury_breakdown_split(company['id'], jwt_token)
from app.bot.formatters import format_treasury_banca_response
from app.bot.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:
@@ -1251,7 +1501,7 @@ async def handle_menu_callback(query, telegram_user_id: int, callback_data: str)
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.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(
@@ -1259,6 +1509,14 @@ async def handle_menu_callback(query, telegram_user_id: int, callback_data: str)
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(
@@ -1272,7 +1530,7 @@ async def handle_menu_callback(query, telegram_user_id: int, callback_data: str)
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.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(
@@ -1280,6 +1538,14 @@ async def handle_menu_callback(query, telegram_user_id: int, callback_data: str)
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(
@@ -1293,7 +1559,7 @@ async def handle_menu_callback(query, telegram_user_id: int, callback_data: str)
from app.bot.helpers import get_cashflow_evolution_data
evolution_data = await get_cashflow_evolution_data(company['id'], jwt_token)
from app.bot.formatters import format_cashflow_evolution_response
from app.bot.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(
@@ -1301,6 +1567,14 @@ async def handle_menu_callback(query, telegram_user_id: int, callback_data: str)
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(
@@ -1309,6 +1583,72 @@ async def handle_menu_callback(query, telegram_user_id: int, callback_data: str)
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(
@@ -1346,10 +1686,22 @@ async def handle_action_callback(query, telegram_user_id: int, callback_data: st
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)
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(
@@ -1897,6 +2249,87 @@ async def button_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
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:"):
@@ -2144,11 +2577,20 @@ async def _handle_sold_view(
await query_or_update.message.reply_text(error_msg)
return
from app.bot.formatters import format_dashboard_response
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:

View File

@@ -344,7 +344,7 @@ async def get_treasury_breakdown_split(
for item in banca_data.get('items', [])
]
return {
result = {
'casa': {
'accounts': casa_accounts,
'total': float(casa_data.get('total', 0))
@@ -355,6 +355,16 @@ async def get_treasury_breakdown_split(
}
}
# Pass through cache metadata if present
if 'cache_hit' in breakdown:
result['cache_hit'] = breakdown['cache_hit']
if 'response_time_ms' in breakdown:
result['response_time_ms'] = breakdown['response_time_ms']
if 'cache_source' in breakdown:
result['cache_source'] = breakdown['cache_source']
return result
except Exception as e:
logger.error(f"Error getting treasury breakdown split: {e}", exc_info=True)
return None
@@ -425,7 +435,7 @@ async def get_clients_with_maturity(
overdue = sum(c['balance'] for c in clients if c.get('daysOverdue', 0) > 0)
in_term = total - overdue
return {
result = {
'clients': clients,
'maturity': {
'in_term': in_term,
@@ -434,6 +444,16 @@ async def get_clients_with_maturity(
}
}
# Pass through cache metadata if present
if 'cache_hit' in maturity_response:
result['cache_hit'] = maturity_response['cache_hit']
if 'response_time_ms' in maturity_response:
result['response_time_ms'] = maturity_response['response_time_ms']
if 'cache_source' in maturity_response:
result['cache_source'] = maturity_response['cache_source']
return result
except Exception as e:
logger.error(f"Error getting clients with maturity: {e}", exc_info=True)
return None
@@ -504,7 +524,7 @@ async def get_suppliers_with_maturity(
overdue = sum(s['balance'] for s in suppliers if s.get('daysOverdue', 0) > 0)
in_term = total - overdue
return {
result = {
'suppliers': suppliers,
'maturity': {
'in_term': in_term,
@@ -513,6 +533,16 @@ async def get_suppliers_with_maturity(
}
}
# Pass through cache metadata if present
if 'cache_hit' in maturity_response:
result['cache_hit'] = maturity_response['cache_hit']
if 'response_time_ms' in maturity_response:
result['response_time_ms'] = maturity_response['response_time_ms']
if 'cache_source' in maturity_response:
result['cache_source'] = maturity_response['cache_source']
return result
except Exception as e:
logger.error(f"Error getting suppliers with maturity: {e}", exc_info=True)
return None
@@ -650,11 +680,21 @@ async def get_cashflow_evolution_data(
'plati_prev': last_12_plati_prev
}
return {
result = {
'performance': performance,
'monthly': monthly
}
# Pass through cache metadata if present
if 'cache_hit' in trends_data:
result['cache_hit'] = trends_data['cache_hit']
if 'response_time_ms' in trends_data:
result['response_time_ms'] = trends_data['response_time_ms']
if 'cache_source' in trends_data:
result['cache_source'] = trends_data['cache_source']
return result
except Exception as e:
logger.error(f"Error getting cashflow evolution data: {e}", exc_info=True)
return None