Initial commit: ROA2WEB - FastAPI + Vue.js + Telegram Bot
Modern ERP Reports Application with microservices architecture Tech Stack: - Backend: FastAPI + python-oracledb (Oracle DB integration) - Frontend: Vue.js 3 + PrimeVue + Vite - Telegram Bot: python-telegram-bot + SQLite - Infrastructure: Shared database pool, JWT authentication, SSH tunnel Features: - FastAPI backend with async Oracle connection pool - Vue.js 3 responsive frontend with PrimeVue components - Telegram bot alternative interface - Microservices architecture with shared components - Complete deployment support (Linux Docker + Windows IIS) - Comprehensive testing (Playwright E2E + pytest) Repository Structure: - reports-app/ - Main application (backend, frontend, telegram-bot) - shared/ - Shared components (database pool, auth, utils) - deployment/ - Deployment scripts (Linux & Windows) - docs/ - Project documentation - security/ - Security scanning and git hooks
This commit is contained in:
0
reports-app/backend/app/routers/__init__.py
Normal file
0
reports-app/backend/app/routers/__init__.py
Normal file
177
reports-app/backend/app/routers/companies.py
Normal file
177
reports-app/backend/app/routers/companies.py
Normal file
@@ -0,0 +1,177 @@
|
||||
"""
|
||||
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 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
|
||||
|
||||
@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:
|
||||
companies = []
|
||||
|
||||
# Obține toate companiile pentru utilizator direct din query-ul complet
|
||||
# Ignorăm lista din JWT și recalculăm direct din Oracle pentru a obține toate cele 63 de companii
|
||||
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': current_user.username.upper()})
|
||||
|
||||
user_row = cursor.fetchone()
|
||||
if not user_row:
|
||||
print(f"User {current_user.username} not found in UTILIZATORI table")
|
||||
return CompanyListResponse(companies=[], total_count=0)
|
||||
|
||||
user_id = user_row[0]
|
||||
print(f"Found user {current_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 {current_user.username}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Eroare la obținerea companiilor din Oracle: {e}")
|
||||
# Fallback: folosim lista din JWT dacă query-ul Oracle eșuează
|
||||
for company_id in current_user.companies:
|
||||
try:
|
||||
id_firma = int(company_id)
|
||||
company = Company(
|
||||
id_firma=id_firma,
|
||||
name=f"Company {id_firma}",
|
||||
schema_name="",
|
||||
fiscal_code="",
|
||||
is_active=True
|
||||
)
|
||||
companies.append(company)
|
||||
except ValueError:
|
||||
# Skip invalid company IDs
|
||||
continue
|
||||
|
||||
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"
|
||||
}
|
||||
327
reports-app/backend/app/routers/dashboard.py
Normal file
327
reports-app/backend/app/routers/dashboard.py
Normal file
@@ -0,0 +1,327 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from typing import 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
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from ..models.dashboard import DashboardSummary, TrendsResponse, TrendData
|
||||
from ..services.dashboard_service import DashboardService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/summary", response_model=DashboardSummary)
|
||||
async def get_dashboard_summary(
|
||||
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
|
||||
"""
|
||||
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
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea datelor dashboard: {str(e)}")
|
||||
|
||||
@router.get("/trends", response_model=TrendsResponse)
|
||||
async def get_dashboard_trends(
|
||||
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ă"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
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
|
||||
- Returnează date pentru grafice de trenduri
|
||||
"""
|
||||
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}")
|
||||
|
||||
# Validează perioada
|
||||
valid_periods = ["7d", "30d", "ytd", "12m"]
|
||||
if period not in valid_periods:
|
||||
raise HTTPException(
|
||||
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)
|
||||
|
||||
except ValueError as e:
|
||||
logger.error(f"Value error in trends endpoint: {str(e)}")
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Eroare la obținerea trendurilor: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea trendurilor: {str(e)}")
|
||||
|
||||
@router.get("/detailed-data")
|
||||
async def get_detailed_data(
|
||||
company: str = Query(description="Codul firmei"),
|
||||
data_type: str = Query(description="Tipul de date: clients, suppliers, treasury"),
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=25, ge=1, le=100),
|
||||
search: str = Query(default="", description="Termen de căutare"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Obține date detaliate pentru tabelele din dashboard
|
||||
"""
|
||||
logger.info(f"[ROUTER] detailed-data called: company={company}, data_type={data_type}")
|
||||
try:
|
||||
if company not in current_user.companies:
|
||||
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
|
||||
|
||||
logger.info(f"[ROUTER] Calling DashboardService.get_detailed_data")
|
||||
result = await DashboardService.get_detailed_data(
|
||||
company=company,
|
||||
data_type=data_type,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
search=search
|
||||
)
|
||||
|
||||
logger.info(f"[ROUTER] Service returned: {len(result.get('data', []))} rows")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Eroare la obținerea datelor detaliate: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/performance")
|
||||
async def get_performance(
|
||||
company: int = Query(..., description="ID-ul firmei"),
|
||||
period: str = Query("7d", regex="^(7d|1m|3m|6m|ytd|12m)$", description="Perioada pentru analiză"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Returnează date performanță pentru perioada selectată
|
||||
|
||||
- Necesită autentificare JWT
|
||||
- Returnează grafice încasări vs plăți pentru perioada selectată
|
||||
- Calculează indicatori: rata încasării, cash conversion, working capital
|
||||
"""
|
||||
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_performance_data(company, period)
|
||||
|
||||
# Convert to Chart.js compatible format
|
||||
return {
|
||||
"labels": result.get("labels", []),
|
||||
"datasets": [{
|
||||
"data": result.get("data", []),
|
||||
"label": result.get("label", "Performance"),
|
||||
"borderColor": result.get("borderColor", "#3B82F6"),
|
||||
"backgroundColor": result.get("backgroundColor", "rgba(59, 130, 246, 0.1)"),
|
||||
"tension": 0.4
|
||||
}]
|
||||
}
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Eroare la obținerea datelor de performanță: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea datelor de performanță: {str(e)}")
|
||||
|
||||
@router.get("/cashflow")
|
||||
async def get_cashflow(
|
||||
company: int = Query(..., description="ID-ul firmei"),
|
||||
period: str = Query("7d", regex="^(7d|1m|3m|6m)$", description="Perioada pentru previziune"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Returnează previziune cash flow pentru perioada selectată
|
||||
|
||||
- Necesită autentificare JWT
|
||||
- Analizează scadențele viitoare pentru calculul cash flow-ului
|
||||
- Identifică zilele critice cu deficit de cash
|
||||
"""
|
||||
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_cashflow_forecast(company, period)
|
||||
|
||||
# Convert to Chart.js compatible format
|
||||
return {
|
||||
"labels": result.get("labels", []),
|
||||
"datasets": [{
|
||||
"data": result.get("data", []),
|
||||
"label": result.get("label", "Cash Flow"),
|
||||
"borderColor": result.get("borderColor", "#10B981"),
|
||||
"backgroundColor": result.get("backgroundColor", "rgba(16, 185, 129, 0.1)"),
|
||||
"tension": 0.4
|
||||
}]
|
||||
}
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Eroare la obținerea previziunii cash flow: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea previziunii cash flow: {str(e)}")
|
||||
|
||||
@router.get("/maturity")
|
||||
async def get_maturity_analysis(
|
||||
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)
|
||||
):
|
||||
"""
|
||||
Returnează analiza scadențelor pentru orizontul de planificare selectat
|
||||
|
||||
- Necesită autentificare JWT
|
||||
- Logică: Include TOATE restanțele + scadențele viitoare din perioada selectată
|
||||
- Perioade disponibile:
|
||||
* 7d: Toate restanțele + scadențe următoarelor 7 zile
|
||||
* 1m: Toate restanțele + scadențe următoarelor 30 zile
|
||||
* 3m: Toate restanțele + scadențe următoarelor 90 zile
|
||||
* 6m: Toate restanțele + scadențe următoarelor 180 zile
|
||||
* 12m: Toate restanțele + scadențe următoarelor 365 zile
|
||||
* all: Toate soldurile (fără filtru)
|
||||
- Compară scadențele clienți vs furnizori
|
||||
- Calculează balanța și oferă recomandări
|
||||
- Returnează metadate cu statistici complete
|
||||
"""
|
||||
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
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Eroare la obținerea analizei scadențelor: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea analizei scadențelor: {str(e)}")
|
||||
|
||||
@router.get("/monthly-flows")
|
||||
async def get_monthly_flows(
|
||||
company: int = Query(..., description="ID-ul firmei"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Returnează fluxurile lunare pentru firma selectată
|
||||
|
||||
- Necesită autentificare JWT
|
||||
- Returnează date pentru analiza fluxurilor lunare
|
||||
"""
|
||||
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_monthly_flows(company)
|
||||
return result
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Eroare la obținerea fluxurilor lunare: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea fluxurilor lunare: {str(e)}")
|
||||
|
||||
@router.get("/treasury-breakdown")
|
||||
async def get_treasury_breakdown(
|
||||
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
|
||||
"""
|
||||
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
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Eroare la obținerea defalcării trezoreriei: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea defalcării trezoreriei: {str(e)}")
|
||||
|
||||
@router.get("/net-balance-breakdown")
|
||||
async def get_net_balance_breakdown(
|
||||
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
|
||||
"""
|
||||
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
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Eroare la obținerea defalcării balanței nete: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea defalcării balanței nete: {str(e)}")
|
||||
|
||||
@router.get("/current-period")
|
||||
async def get_current_period(
|
||||
company: int = Query(..., description="ID-ul firmei"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Returnează perioada curentă (an și lună) din calendarul Oracle
|
||||
|
||||
- Necesită autentificare JWT
|
||||
- Returnează anul, luna și perioada curentă în format YYYY-MM
|
||||
- Folosit pentru afișarea lunii curente în dashboard
|
||||
"""
|
||||
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_current_period(company)
|
||||
return result
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Eroare la obținerea perioadei curente: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea perioadei curente: {str(e)}")
|
||||
143
reports-app/backend/app/routers/invoices.py
Normal file
143
reports-app/backend/app/routers/invoices.py
Normal file
@@ -0,0 +1,143 @@
|
||||
"""
|
||||
API Router pentru facturi
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from typing import List, Optional
|
||||
from datetime import date
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '../../../../shared'))
|
||||
|
||||
from auth.dependencies import get_current_user, require_company_access
|
||||
from auth.models import CurrentUser
|
||||
from ..models.invoice import InvoiceFilter, InvoiceListResponse, InvoiceSummary
|
||||
from ..services.invoice_service import InvoiceService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/", response_model=InvoiceListResponse)
|
||||
async def get_invoices(
|
||||
company: str = Query(description="Codul firmei"),
|
||||
partner_type: str = Query("CLIENTI", description="CLIENTI sau FURNIZORI"),
|
||||
date_from: Optional[str] = Query(None, description="Data început (YYYY-MM-DD)"),
|
||||
date_to: Optional[str] = Query(None, description="Data sfârșit (YYYY-MM-DD)"),
|
||||
partner_name: Optional[str] = Query(None, description="Filtru nume partener"),
|
||||
only_unpaid: bool = Query(True, description="Doar facturile neachitate"),
|
||||
min_amount: Optional[float] = Query(None, description="Suma minimă"),
|
||||
max_amount: Optional[float] = Query(None, description="Suma maximă"),
|
||||
page: int = Query(1, ge=1, description="Pagina"),
|
||||
page_size: int = Query(50, ge=1, le=1000, description="Mărimea paginii"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Obține lista de facturi pentru o firmă
|
||||
|
||||
- Necesită autentificare JWT
|
||||
- Utilizatorul trebuie să aibă acces la firma specificată
|
||||
- Suportă filtrare și paginare
|
||||
"""
|
||||
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}")
|
||||
|
||||
# Convertește string-urile de date în obiecte date
|
||||
date_from_obj = None
|
||||
date_to_obj = None
|
||||
|
||||
if date_from:
|
||||
try:
|
||||
date_from_obj = date.fromisoformat(date_from)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Formatul datei de început este invalid. Folosiți YYYY-MM-DD")
|
||||
|
||||
if date_to:
|
||||
try:
|
||||
date_to_obj = date.fromisoformat(date_to)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Formatul datei de sfârșit este invalid. Folosiți YYYY-MM-DD")
|
||||
|
||||
filter_params = InvoiceFilter(
|
||||
company=company,
|
||||
partner_type=partner_type,
|
||||
date_from=date_from_obj,
|
||||
date_to=date_to_obj,
|
||||
partner_name=partner_name,
|
||||
only_unpaid=only_unpaid,
|
||||
min_amount=min_amount,
|
||||
max_amount=max_amount,
|
||||
page=page,
|
||||
page_size=page_size
|
||||
)
|
||||
|
||||
result = await InvoiceService.get_invoices(filter_params, current_user.username)
|
||||
return result
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea facturilor: {str(e)}")
|
||||
|
||||
@router.get("/summary", response_model=InvoiceSummary)
|
||||
async def get_invoices_summary(
|
||||
company: str = Query(description="Codul firmei"),
|
||||
partner_type: str = Query("CLIENTI", description="CLIENTI sau FURNIZORI"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""Obține rezumatul facturilor pentru dashboard"""
|
||||
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 InvoiceService.get_invoice_summary(company, partner_type, current_user.username)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea rezumatului facturilor: {str(e)}")
|
||||
|
||||
@router.get("/{invoice_number}")
|
||||
async def get_invoice_details(
|
||||
invoice_number: str,
|
||||
company: str = Query(description="Codul firmei"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""Obține detaliile unei facturi specifice"""
|
||||
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 InvoiceService.get_invoice_details(company, invoice_number, current_user.username)
|
||||
return result
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea detaliilor facturii: {str(e)}")
|
||||
|
||||
@router.get("/export/{format}")
|
||||
async def export_invoices(
|
||||
format: str,
|
||||
company: str = Query(description="Codul firmei"),
|
||||
partner_type: str = Query("CLIENTI", description="CLIENTI sau FURNIZORI"),
|
||||
date_from: Optional[str] = Query(None, description="Data început (YYYY-MM-DD)"),
|
||||
date_to: Optional[str] = Query(None, description="Data sfârșit (YYYY-MM-DD)"),
|
||||
partner_name: Optional[str] = Query(None, description="Filtru nume partener"),
|
||||
only_unpaid: bool = Query(True, description="Doar facturile neachitate"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Export facturi în format specificat (excel, pdf, csv)
|
||||
Această funcție va fi implementată în viitor
|
||||
"""
|
||||
# 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}")
|
||||
|
||||
# Verifică formatul
|
||||
if format not in ["excel", "pdf", "csv"]:
|
||||
raise HTTPException(status_code=400, detail="Format invalid. Formatele suportate sunt: excel, pdf, csv")
|
||||
|
||||
# Pentru moment, returnează o eroare că funcția nu este implementată
|
||||
raise HTTPException(status_code=501, detail=f"Export în format {format} nu este încă implementat")
|
||||
559
reports-app/backend/app/routers/telegram.py
Normal file
559
reports-app/backend/app/routers/telegram.py
Normal file
@@ -0,0 +1,559 @@
|
||||
"""
|
||||
API Router pentru Telegram Bot Integration
|
||||
Furnizează endpoint-uri pentru autentificare, linking și export rapoarte pentru Telegram bot
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from typing import List, Optional, Dict, Any
|
||||
import sys
|
||||
import os
|
||||
import secrets
|
||||
import string
|
||||
import httpx
|
||||
from datetime import datetime, timedelta
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '../../../../shared'))
|
||||
|
||||
from auth.dependencies import get_current_user
|
||||
from auth.models import CurrentUser
|
||||
from auth.jwt_handler import jwt_handler
|
||||
from database.oracle_pool import oracle_pool
|
||||
|
||||
# Telegram bot internal API URL (running on same server)
|
||||
TELEGRAM_BOT_INTERNAL_API = os.getenv("TELEGRAM_BOT_INTERNAL_API", "http://localhost:8002")
|
||||
|
||||
router = APIRouter(redirect_slashes=False)
|
||||
|
||||
# ==================== Schemas ====================
|
||||
|
||||
class GenerateCodeRequest(BaseModel):
|
||||
"""Request pentru generarea unui cod de linking"""
|
||||
telegram_user_id: int = Field(description="ID-ul utilizatorului Telegram")
|
||||
telegram_username: Optional[str] = Field(default=None, description="Username-ul Telegram")
|
||||
telegram_first_name: Optional[str] = Field(default=None, description="Prenumele utilizatorului")
|
||||
telegram_last_name: Optional[str] = Field(default=None, description="Numele utilizatorului")
|
||||
|
||||
|
||||
class GenerateCodeResponse(BaseModel):
|
||||
"""Response pentru generarea unui cod de linking"""
|
||||
linking_code: str = Field(description="Codul de linking generat (8 caractere)")
|
||||
expires_at: datetime = Field(description="Data și ora expirării codului")
|
||||
expires_in_minutes: int = Field(description="Minutele până la expirare")
|
||||
|
||||
|
||||
class VerifyUserRequest(BaseModel):
|
||||
"""
|
||||
Request pentru verificarea utilizatorului în Oracle
|
||||
|
||||
Suportă 2 flow-uri:
|
||||
1. Auto-linking (recomandat): doar linking_code și oracle_username
|
||||
- Bot-ul verifică codul în SQLite, extrage oracle_username
|
||||
- Backend face lookup în Oracle fără verificare parolă
|
||||
- Codul valid este proof-of-authorization
|
||||
|
||||
2. Full verification (opțional): username, password, linking_code
|
||||
- Verificare completă cu parolă în Oracle
|
||||
"""
|
||||
linking_code: str = Field(description="Codul de linking de la /generate-code")
|
||||
oracle_username: Optional[str] = Field(default=None, description="Username Oracle (pentru auto-linking)")
|
||||
username: Optional[str] = Field(default=None, description="Username pentru verificare completă")
|
||||
password: Optional[str] = Field(default=None, description="Parolă pentru verificare completă")
|
||||
|
||||
|
||||
class VerifyUserResponse(BaseModel):
|
||||
"""Response pentru verificarea utilizatorului"""
|
||||
success: bool = Field(description="True dacă verificarea a avut succes")
|
||||
access_token: Optional[str] = Field(default=None, description="JWT access token")
|
||||
refresh_token: Optional[str] = Field(default=None, description="JWT refresh token")
|
||||
user: Optional[Dict[str, Any]] = Field(default=None, description="Detalii utilizator")
|
||||
message: str = Field(description="Mesaj de status")
|
||||
|
||||
|
||||
class RefreshTokenRequest(BaseModel):
|
||||
"""Request pentru refresh JWT token"""
|
||||
refresh_token: str = Field(description="Refresh token-ul obținut la autentificare")
|
||||
|
||||
|
||||
class RefreshTokenResponse(BaseModel):
|
||||
"""Response pentru refresh token"""
|
||||
access_token: str = Field(description="Noul JWT access token")
|
||||
expires_in: int = Field(description="Timpul de expirare în secunde")
|
||||
token_type: str = Field(default="bearer", description="Tipul token-ului")
|
||||
|
||||
|
||||
class ExportReportRequest(BaseModel):
|
||||
"""Request pentru exportul unui raport"""
|
||||
company_id: int = Field(description="ID-ul firmei")
|
||||
report_type: str = Field(description="Tipul raportului (invoices, payments, dashboard)")
|
||||
format: str = Field(default="excel", description="Formatul exportului (excel, pdf, csv)")
|
||||
filters: Optional[Dict[str, Any]] = Field(default=None, description="Filtre pentru raport")
|
||||
|
||||
|
||||
class ExportReportResponse(BaseModel):
|
||||
"""Response pentru exportul raportului"""
|
||||
success: bool = Field(description="True dacă exportul a avut succes")
|
||||
file_url: Optional[str] = Field(default=None, description="URL-ul fișierului generat")
|
||||
file_name: Optional[str] = Field(default=None, description="Numele fișierului generat")
|
||||
file_size_bytes: Optional[int] = Field(default=None, description="Mărimea fișierului în bytes")
|
||||
message: str = Field(description="Mesaj de status")
|
||||
|
||||
|
||||
# ==================== Helper Functions ====================
|
||||
|
||||
def generate_linking_code(length: int = 8) -> str:
|
||||
"""
|
||||
Generează un cod alfanumeric aleatoriu pentru linking
|
||||
|
||||
Args:
|
||||
length: Lungimea codului (default: 8)
|
||||
|
||||
Returns:
|
||||
Codul generat (uppercase alphanumeric)
|
||||
"""
|
||||
alphabet = string.ascii_uppercase + string.digits
|
||||
# Exclude caractere care pot fi confundate: 0, O, I, 1
|
||||
alphabet = alphabet.replace('0', '').replace('O', '').replace('I', '').replace('1', '')
|
||||
return ''.join(secrets.choice(alphabet) for _ in range(length))
|
||||
|
||||
|
||||
async def get_oracle_user_by_username(username: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Obține informații despre utilizator din Oracle FĂRĂ verificare parolă.
|
||||
|
||||
Folosit pentru auto-linking când utilizatorul a fost deja autentificat
|
||||
prin generarea unui linking code valid în aplicația web.
|
||||
|
||||
Args:
|
||||
username: Username-ul utilizatorului Oracle
|
||||
|
||||
Returns:
|
||||
Dict cu informații despre utilizator sau None dacă nu există
|
||||
"""
|
||||
try:
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# Obține detalii utilizator
|
||||
cursor.execute("""
|
||||
SELECT ID_UTIL, UTILIZATOR
|
||||
FROM UTILIZATORI
|
||||
WHERE UPPER(UTILIZATOR) = :username
|
||||
""", {'username': username.upper()})
|
||||
|
||||
user_row = cursor.fetchone()
|
||||
if not user_row:
|
||||
return None
|
||||
|
||||
user_id = user_row[0]
|
||||
actual_username = user_row[1]
|
||||
|
||||
# Obține companiile utilizatorului
|
||||
cursor.execute("""
|
||||
SELECT A.ID_FIRMA, A.FIRMA
|
||||
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_result = cursor.fetchall()
|
||||
companies = [str(row[0]) for row in companies_result]
|
||||
|
||||
return {
|
||||
'user_id': user_id,
|
||||
'username': actual_username,
|
||||
'companies': companies,
|
||||
'permissions': ['read', 'reports']
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting Oracle user by username: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def verify_oracle_user(username: str, password: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Verifică utilizatorul în Oracle folosind pack_drepturi.verificautilizator
|
||||
|
||||
Args:
|
||||
username: Username-ul utilizatorului
|
||||
password: Parola utilizatorului
|
||||
|
||||
Returns:
|
||||
Dict cu informații despre utilizator sau None dacă verificarea eșuează
|
||||
"""
|
||||
try:
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# Verifică autentificarea
|
||||
cursor.execute("""
|
||||
SELECT pack_drepturi.verificautilizator(:username, :password)
|
||||
FROM DUAL
|
||||
""", {
|
||||
'username': username.upper(),
|
||||
'password': password
|
||||
})
|
||||
|
||||
result = cursor.fetchone()
|
||||
verification_result = result[0] if result else -1
|
||||
|
||||
if verification_result == -1:
|
||||
return None
|
||||
|
||||
# Obține detalii utilizator
|
||||
cursor.execute("""
|
||||
SELECT ID_UTIL, UTILIZATOR
|
||||
FROM UTILIZATORI
|
||||
WHERE UPPER(UTILIZATOR) = :username
|
||||
""", {'username': username.upper()})
|
||||
|
||||
user_row = cursor.fetchone()
|
||||
if not user_row:
|
||||
return None
|
||||
|
||||
user_id = user_row[0]
|
||||
|
||||
# Obține companiile utilizatorului
|
||||
cursor.execute("""
|
||||
SELECT A.ID_FIRMA, A.FIRMA
|
||||
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_result = cursor.fetchall()
|
||||
companies = [str(row[0]) for row in companies_result]
|
||||
|
||||
return {
|
||||
'user_id': user_id,
|
||||
'username': username,
|
||||
'companies': companies,
|
||||
'permissions': ['read', 'reports']
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error verifying Oracle user: {e}")
|
||||
return None
|
||||
|
||||
|
||||
# ==================== Endpoints ====================
|
||||
|
||||
@router.post("/auth/generate-code", response_model=GenerateCodeResponse)
|
||||
async def generate_linking_code_endpoint(
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Generează un cod de linking pentru conectarea unui utilizator Telegram
|
||||
|
||||
Flow:
|
||||
1. Utilizatorul autentificat în aplicație solicită un cod
|
||||
2. Se generează un cod unic de 8 caractere
|
||||
3. Codul este trimis la Telegram bot pentru salvare în SQLite cu TTL de 15 minute
|
||||
4. Utilizatorul introduce codul în Telegram bot pentru linking
|
||||
|
||||
Note:
|
||||
- Acest endpoint necesită autentificare JWT (utilizatorul trebuie să fie logat în aplicație)
|
||||
- Codul expiră după 15 minute
|
||||
- Fiecare request generează un cod nou (codurile vechi devin invalide)
|
||||
- Nu este nevoie de telegram_user_id în acest moment (utilizatorul nu e încă conectat la Telegram)
|
||||
"""
|
||||
try:
|
||||
# Generează cod unic
|
||||
linking_code = generate_linking_code()
|
||||
|
||||
# Setează expirarea la 15 minute
|
||||
expires_at = datetime.utcnow() + timedelta(minutes=15)
|
||||
expires_in_minutes = 15
|
||||
|
||||
# Salvează codul în database-ul Telegram bot (SQLite) via internal API
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||
save_code_response = await client.post(
|
||||
f"{TELEGRAM_BOT_INTERNAL_API}/internal/save-code",
|
||||
json={
|
||||
"code": linking_code,
|
||||
"telegram_user_id": 0, # Not known yet (user hasn't linked)
|
||||
"oracle_username": current_user.username,
|
||||
"expires_in_minutes": expires_in_minutes
|
||||
}
|
||||
)
|
||||
|
||||
# Accept both 200 (OK) and 201 (Created) as success
|
||||
if save_code_response.status_code not in [200, 201]:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to save code to Telegram bot: {save_code_response.text}"
|
||||
)
|
||||
except httpx.TimeoutException:
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="Telegram bot service is not responding. Please try again later."
|
||||
)
|
||||
except httpx.ConnectError:
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="Cannot connect to Telegram bot service. Please contact administrator."
|
||||
)
|
||||
|
||||
return GenerateCodeResponse(
|
||||
linking_code=linking_code,
|
||||
expires_at=expires_at,
|
||||
expires_in_minutes=expires_in_minutes
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Eroare la generarea codului de linking: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/auth/verify-user", response_model=VerifyUserResponse)
|
||||
async def verify_user_endpoint(request: VerifyUserRequest):
|
||||
"""
|
||||
Verifică utilizatorul în Oracle și returnează JWT tokens
|
||||
|
||||
Suportă 2 flow-uri de autentificare:
|
||||
|
||||
Flow A - Auto-linking (RECOMANDAT):
|
||||
1. Bot verifică linking_code în SQLite (code valid = user s-a autentificat în web app)
|
||||
2. Bot extrage oracle_username din cod
|
||||
3. Bot trimite: {linking_code, oracle_username}
|
||||
4. Backend face lookup în Oracle (FĂRĂ verificare parolă)
|
||||
5. Backend generează și returnează JWT tokens
|
||||
|
||||
Flow B - Full verification (OPȚIONAL):
|
||||
1. Bot cere username și parolă de la user în Telegram
|
||||
2. Bot trimite: {linking_code, username, password}
|
||||
3. Backend verifică credențialele în Oracle
|
||||
4. Backend generează și returnează JWT tokens
|
||||
|
||||
Note:
|
||||
- Acest endpoint NU necesită autentificare JWT (este public pentru bot)
|
||||
- Flow A oferă UX superior (fără re-introducere parolă)
|
||||
- Linking code-ul valid este proof-of-authorization
|
||||
"""
|
||||
try:
|
||||
# Flow A: Auto-linking (oracle_username provided, no password)
|
||||
if request.oracle_username and not request.password:
|
||||
user_data = await get_oracle_user_by_username(request.oracle_username)
|
||||
|
||||
if not user_data:
|
||||
return VerifyUserResponse(
|
||||
success=False,
|
||||
message=f"Utilizatorul {request.oracle_username} nu există în Oracle"
|
||||
)
|
||||
|
||||
# Flow B: Full verification (username + password provided)
|
||||
elif request.username and request.password:
|
||||
user_data = await verify_oracle_user(request.username, request.password)
|
||||
|
||||
if not user_data:
|
||||
return VerifyUserResponse(
|
||||
success=False,
|
||||
message="Username sau parolă incorectă"
|
||||
)
|
||||
|
||||
# Invalid request (missing required fields)
|
||||
else:
|
||||
return VerifyUserResponse(
|
||||
success=False,
|
||||
message="Trebuie furnizat fie oracle_username (auto-linking) fie username+password (verificare completă)"
|
||||
)
|
||||
|
||||
# Generează JWT tokens
|
||||
access_token = jwt_handler.create_access_token(
|
||||
username=user_data['username'],
|
||||
companies=user_data['companies'],
|
||||
user_id=user_data['user_id'],
|
||||
permissions=user_data['permissions']
|
||||
)
|
||||
|
||||
refresh_token = jwt_handler.create_refresh_token(
|
||||
username=user_data['username'],
|
||||
user_id=user_data['user_id']
|
||||
)
|
||||
|
||||
return VerifyUserResponse(
|
||||
success=True,
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token,
|
||||
user={
|
||||
'user_id': user_data['user_id'],
|
||||
'username': user_data['username'],
|
||||
'companies': user_data['companies'],
|
||||
'permissions': user_data['permissions']
|
||||
},
|
||||
message="Autentificare reușită"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Eroare la verificarea utilizatorului: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/auth/refresh-token", response_model=RefreshTokenResponse)
|
||||
async def refresh_token_endpoint(request: RefreshTokenRequest):
|
||||
"""
|
||||
Refresh-uiește un JWT access token folosind refresh token-ul
|
||||
|
||||
Acest endpoint este folosit de Telegram bot pentru a obține un nou access token
|
||||
când cel curent expiră, fără a solicita din nou username/password.
|
||||
|
||||
Flow:
|
||||
1. Botul Telegram detectează că access token-ul a expirat
|
||||
2. Trimite refresh token-ul la acest endpoint
|
||||
3. Se validează refresh token-ul și se generează un nou access token
|
||||
4. Botul stochează noul access token în SQLite
|
||||
|
||||
Note:
|
||||
- Refresh token-ul este valid 7 zile (vs 30 minute pentru access token)
|
||||
- Dacă refresh token-ul expiră, utilizatorul trebuie să se re-autentifice
|
||||
"""
|
||||
try:
|
||||
# Verifică refresh token-ul
|
||||
token_data = jwt_handler.verify_token(request.refresh_token)
|
||||
|
||||
if not token_data or token_data.token_type != "refresh":
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="Refresh token invalid sau expirat"
|
||||
)
|
||||
|
||||
# Obține companiile actualizate din Oracle
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("""
|
||||
SELECT A.ID_FIRMA
|
||||
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': token_data.user_id})
|
||||
|
||||
companies_result = cursor.fetchall()
|
||||
companies = [str(row[0]) for row in companies_result]
|
||||
|
||||
# Generează nou access token
|
||||
new_access_token = jwt_handler.create_access_token(
|
||||
username=token_data.username,
|
||||
companies=companies,
|
||||
user_id=token_data.user_id,
|
||||
permissions=token_data.permissions
|
||||
)
|
||||
|
||||
return RefreshTokenResponse(
|
||||
access_token=new_access_token,
|
||||
expires_in=jwt_handler.access_token_expire_minutes * 60,
|
||||
token_type="bearer"
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Eroare la refresh token: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/export", response_model=ExportReportResponse)
|
||||
async def export_report_endpoint(
|
||||
request: ExportReportRequest,
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Exportă un raport în format Excel, PDF sau CSV
|
||||
|
||||
Acest endpoint este folosit de Telegram bot pentru a genera rapoarte
|
||||
și a le trimite utilizatorului.
|
||||
|
||||
Flow:
|
||||
1. Botul trimite cerere de export cu parametrii raportului
|
||||
2. Se validează că utilizatorul are acces la firma specificată
|
||||
3. Se generează raportul în formatul solicitat
|
||||
4. Se returnează URL-ul sau conținutul fișierului
|
||||
|
||||
Tipuri de rapoarte suportate:
|
||||
- invoices: Facturi (cu filtre: dată, status, client)
|
||||
- payments: Încasări (cu filtre: dată, metodă plată)
|
||||
- dashboard: Statistici dashboard (rezumat)
|
||||
|
||||
Formate suportate:
|
||||
- excel: XLSX (cel mai complet)
|
||||
- pdf: PDF (pentru printing)
|
||||
- csv: CSV (pentru import în alte sisteme)
|
||||
|
||||
Note:
|
||||
- Utilizatorul trebuie să aibă acces la firma specificată
|
||||
- Fișierele generate sunt temporare (șterse după 1 oră)
|
||||
"""
|
||||
try:
|
||||
# Verifică accesul la firmă
|
||||
company_id_str = str(request.company_id)
|
||||
if company_id_str not in current_user.companies:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"Nu aveți acces la firma {request.company_id}"
|
||||
)
|
||||
|
||||
# TODO: Implementare export în funcție de report_type și format
|
||||
# Deocamdată returnăm un placeholder
|
||||
|
||||
return ExportReportResponse(
|
||||
success=True,
|
||||
file_url=f"/api/telegram/downloads/report_{request.report_type}_{request.company_id}.{request.format}",
|
||||
file_name=f"raport_{request.report_type}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.{request.format}",
|
||||
file_size_bytes=0,
|
||||
message=f"Raport {request.report_type} generat cu succes în format {request.format}"
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Eroare la generarea raportului: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def telegram_health_check():
|
||||
"""
|
||||
Health check pentru routerul Telegram
|
||||
Verifică conectivitatea la Oracle și disponibilitatea serviciilor
|
||||
"""
|
||||
try:
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SELECT 1 FROM DUAL")
|
||||
|
||||
return {
|
||||
"status": "healthy",
|
||||
"service": "telegram-router",
|
||||
"database": "connected",
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"status": "degraded",
|
||||
"service": "telegram-router",
|
||||
"database": f"error: {str(e)}",
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
67
reports-app/backend/app/routers/treasury.py
Normal file
67
reports-app/backend/app/routers/treasury.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from typing import Optional
|
||||
from datetime import date
|
||||
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 ..models.treasury import RegisterFilter, RegisterListResponse
|
||||
from ..services.treasury_service import TreasuryService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/bank-cash-register", response_model=RegisterListResponse)
|
||||
async def get_bank_cash_register(
|
||||
company: str = Query(description="Codul firmei"),
|
||||
date_from: Optional[str] = Query(None, description="Data început (YYYY-MM-DD)"),
|
||||
date_to: Optional[str] = Query(None, description="Data sfârșit (YYYY-MM-DD)"),
|
||||
partner_name: Optional[str] = Query(None, description="Filtru nume partener"),
|
||||
page: int = Query(1, ge=1, description="Pagina"),
|
||||
page_size: int = Query(50, ge=1, le=1000, description="Mărimea paginii"),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Obține registrul de casă și bancă
|
||||
|
||||
- Necesită autentificare JWT
|
||||
- Suportă filtrare și paginare
|
||||
"""
|
||||
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}")
|
||||
|
||||
# Convertește datele
|
||||
date_from_obj = None
|
||||
date_to_obj = None
|
||||
|
||||
if date_from:
|
||||
try:
|
||||
date_from_obj = date.fromisoformat(date_from)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Format dată început invalid")
|
||||
|
||||
if date_to:
|
||||
try:
|
||||
date_to_obj = date.fromisoformat(date_to)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Format dată sfârșit invalid")
|
||||
|
||||
filter_params = RegisterFilter(
|
||||
company=company,
|
||||
date_from=date_from_obj,
|
||||
date_to=date_to_obj,
|
||||
partner_name=partner_name,
|
||||
page=page,
|
||||
page_size=page_size
|
||||
)
|
||||
|
||||
result = await TreasuryService.get_bank_cash_register(filter_params, current_user.username)
|
||||
return result
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Eroare la obținerea registrului: {str(e)}")
|
||||
Reference in New Issue
Block a user