Files
roa2web-service-auto/reports-app/backend/app/routers/companies.py
Marius Mutu 1378ee1e6a Implement hybrid two-tier cache system with full monitoring and Telegram bot enhancements
Cache System (Backend):
- Implemented two-tier hybrid cache: L1 (in-memory) + L2 (SQLite)
- L1 cache: Fast dictionary-based with 5-minute TTL for hot data
- L2 cache: Persistent SQLite with 1-hour TTL for warm data
- Cache decorator with automatic tier management and fallback
- Cache key generation with per-user isolation
- Event monitoring system for cache statistics
- Cache benchmarking utilities for performance testing
- Added cache management endpoints: /api/cache/stats, /api/cache/clear, /api/cache/benchmark
- Cache configuration via environment variables (CACHE_ENABLED, CACHE_L1_TTL, etc.)

Backend Services:
- Updated dashboard_service to use @cached decorator with request context
- Added cache support to invoice_service and treasury_service
- Integrated cache manager into main.py with lifespan events
- Added Request parameter to service methods for cache metadata

Frontend Enhancements:
- New CacheStatsView.vue for real-time cache monitoring dashboard
- Cache store (cacheStore.js) for state management
- Updated router to include /cache-stats route
- Navigation updates in DashboardHeader and HamburgerMenu
- Cache stats accessible from main navigation

Telegram Bot Improvements:
- Enhanced formatters with YTD comparison data
- Improved menu navigation and button layout
- Better error handling and user feedback
- Bot startup improvements with graceful shutdown

Auth & Middleware:
- Enhanced middleware with cache metadata injection
- Improved request state handling for cache source tracking

Development:
- Updated start-dev.sh with better error handling
- Added TELEGRAM_EMAIL_AUTH_PLAN.md documentation
- Updated requirements.txt with aiosqlite for async SQLite

Performance:
- L1 cache provides <1ms response for hot data
- L2 cache provides ~5ms response for warm data
- Database queries only for cold data or cache misses
- Cache hit rates tracked and displayed in real-time

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 22:42:00 +02:00

176 lines
6.2 KiB
Python

"""
API Router pentru managementul firmelor
"""
from fastapi import APIRouter, Depends, HTTPException, Request
from typing import List, Optional
import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), '../../../../shared'))
from auth.dependencies import get_current_user
from auth.models import CurrentUser
from database.oracle_pool import oracle_pool
from ..cache.decorators import cached
from pydantic import BaseModel
router = APIRouter(redirect_slashes=False)
class Company(BaseModel):
"""Model pentru firmă"""
id_firma: int # Cheia primară
name: str # Numele firmei
schema_name: str # Schema Oracle
fiscal_code: Optional[str] = None
is_active: bool = True
class CompanyListResponse(BaseModel):
"""Răspuns pentru lista de firme"""
companies: List[Company]
total_count: int
@cached(cache_type='companies', key_params=['username'])
async def _get_user_companies_data(username: str) -> List[Company]:
"""
Obține lista companiilor pentru utilizator (CACHED 30 min)
Helper function cached separate de endpoint pentru a permite caching
"""
companies = []
# Obține toate companiile pentru utilizator direct din query-ul complet
async with oracle_pool.get_connection() as connection:
with connection.cursor() as cursor:
try:
# Primul pas: obținem ID-ul utilizatorului din UTILIZATORI
cursor.execute("""
SELECT ID_UTIL, UTILIZATOR
FROM UTILIZATORI
WHERE UPPER(UTILIZATOR) = :username
""", {'username': username.upper()})
user_row = cursor.fetchone()
if not user_row:
print(f"User {username} not found in UTILIZATORI table")
return []
user_id = user_row[0]
print(f"Found user {username} with ID: {user_id}")
# Al doilea pas: obținem TOATE companiile pentru programul 2
cursor.execute("""
SELECT A.ID_FIRMA, A.FIRMA, A.SCHEMA, A.COD_FISCAL
FROM V_NOM_FIRME A
WHERE A.ID_FIRMA IN (
SELECT ID_FIRMA
FROM VDEF_UTIL_FIRME
WHERE ID_PROGRAM = 2 AND ID_UTIL = :user_id
)
ORDER BY A.FIRMA
""", {'user_id': user_id})
companies_rows = cursor.fetchall()
for row in companies_rows:
id_firma = row[0]
firma_name = row[1]
schema = row[2]
fiscal_code = row[3] # Poate fi NULL
company = Company(
id_firma=id_firma,
name=firma_name,
schema_name=schema,
fiscal_code=fiscal_code,
is_active=True
)
companies.append(company)
print(f"Found {len(companies)} companies for user {username}")
except Exception as e:
print(f"Eroare la obținerea companiilor din Oracle: {e}")
return companies
@router.get("", response_model=CompanyListResponse)
@router.get("/", response_model=CompanyListResponse)
async def get_user_companies(
request: Request,
current_user: CurrentUser = Depends(get_current_user)
):
"""
Obține lista firmelor la care utilizatorul are acces cu detalii complete
"""
print(f"[COMPANIES DEBUG] Request state: user={getattr(request.state, 'user', 'NOT_SET')}, is_authenticated={getattr(request.state, 'is_authenticated', 'NOT_SET')}")
print(f"[COMPANIES DEBUG] Authorization header: {request.headers.get('Authorization', 'NOT_SET')}")
try:
# Call cached helper function
companies = await _get_user_companies_data(current_user.username)
return CompanyListResponse(
companies=companies,
total_count=len(companies)
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Eroare la obținerea listei de firme: {str(e)}")
@router.get("/{company_id}", response_model=Company)
async def get_company_details(
company_id: str,
current_user: CurrentUser = Depends(get_current_user)
):
"""
Obține detaliile unei firme specifice
"""
try:
# Verifică dacă utilizatorul are acces la firma specificată
if company_id not in current_user.companies:
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company_id}")
async with oracle_pool.get_connection() as connection:
with connection.cursor() as cursor:
# Query pentru detaliile firmei
company_query = """
SELECT ID_FIRMA, FIRMA, SCHEMA, COD_FISCAL
FROM V_NOM_FIRME
WHERE ID_FIRMA = :company_id
"""
cursor.execute(company_query, {'company_id': int(company_id)})
row = cursor.fetchone()
if not row:
raise HTTPException(status_code=404, detail=f"Firma {company_id} nu a fost găsită")
return Company(
id_firma=row[0],
name=row[1],
schema_name=row[2],
fiscal_code=row[3] or "",
is_active=True
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Eroare la obținerea detaliilor firmei: {str(e)}")
@router.get("/{company_id}/validate")
async def validate_company_access(
company_id: str,
current_user: CurrentUser = Depends(get_current_user)
):
"""
Validează dacă utilizatorul are acces la o firmă specificată
"""
has_access = company_id in current_user.companies
return {
"company_id": company_id,
"has_access": has_access,
"user": current_user.username,
"message": "Acces validat" if has_access else "Acces refuzat"
}