feat: multi-Oracle server support with runtime switching

Complete implementation of multi-server Oracle database support:

Backend:
- Multi-pool Oracle with lazy loading per server
- Email-to-server cache for automatic server discovery
- JWT tokens include server_id claim
- /auth/check-identity and /auth/check-email endpoints
- /auth/my-servers endpoint for listing user's accessible servers
- Server switch with password re-authentication

Frontend:
- New ServerSelector component for header dropdown
- Multi-step login flow (identity → server → password)
- Server switching from header with password modal
- Mobile drawer menu with server selection
- Dark mode support for all new components
- URL bookmark support with ?server= query param

Scripts:
- Unified start.sh replacing start-prod.sh/start-test.sh
- Unified ssh-tunnel.sh with multi-server support
- Updated status.sh for new architecture

Tests:
- E2E tests for multi-server and single-server login flows
- Backend unit tests for all new endpoints
- Oracle multi-pool integration tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-01-26 22:39:06 +00:00
parent 5f99ee2fd0
commit b137e80b71
102 changed files with 9398 additions and 2787 deletions

View File

@@ -278,14 +278,14 @@ class FinancialIndicatorsService:
"""
@staticmethod
@cached(cache_type='schema', key_params=['company_id'])
async def _get_schema(company_id: int) -> str:
@cached(cache_type='schema', key_params=['company_id', 'server_id'])
async def _get_schema(company_id: int, server_id: Optional[str] = None) -> str:
"""
Obține schema pentru company_id (CACHED PERMANENT)
Schema este stocată permanent în cache deoarece nu se schimbă.
"""
async with oracle_pool.get_connection() as connection:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
schema_query = """
SELECT schema
@@ -319,11 +319,12 @@ class FinancialIndicatorsService:
return f"SUM(CASE WHEN ({conditions}) THEN NVL({column}, 0) ELSE 0 END)"
@staticmethod
@cached(cache_type='fin_balance_sheet', key_params=['company_id', 'luna', 'an'])
@cached(cache_type='fin_balance_sheet', key_params=['company_id', 'luna', 'an', 'server_id'])
async def get_balance_sheet_aggregates(
company_id: int,
luna: int,
an: int
an: int,
server_id: Optional[str] = None
) -> BalanceSheetAggregates:
"""
Obține soldurile agregate din balanța de verificare pentru calculul
@@ -343,9 +344,9 @@ class FinancialIndicatorsService:
Raises:
ValueError: Dacă schema nu este găsită pentru firma specificată
"""
schema = await FinancialIndicatorsService._get_schema(company_id)
schema = await FinancialIndicatorsService._get_schema(company_id, server_id)
async with oracle_pool.get_connection() as connection:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
# Construim query-ul cu CASE pentru fiecare categorie
# Soldurile din VBAL: SOLDDEB (sold debitor), SOLDCRED (sold creditor)
@@ -546,11 +547,12 @@ class FinancialIndicatorsService:
return aggregates
@staticmethod
@cached(cache_type='fin_achizitii', key_params=['company_id', 'luna', 'an'])
@cached(cache_type='fin_achizitii', key_params=['company_id', 'luna', 'an', 'server_id'])
async def get_achizitii_ytd(
company_id: int,
luna: int,
an: int
an: int,
server_id: Optional[str] = None
) -> Decimal:
"""
Calculează totalul achizițiilor YTD din Registrul Jurnal (ACT).
@@ -575,9 +577,9 @@ class FinancialIndicatorsService:
Returns:
Total achiziții YTD fără TVA (Decimal)
"""
schema = await FinancialIndicatorsService._get_schema(company_id)
schema = await FinancialIndicatorsService._get_schema(company_id, server_id)
async with oracle_pool.get_connection() as connection:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
query = f"""
SELECT
@@ -611,11 +613,12 @@ class FinancialIndicatorsService:
return achizitii_total
@staticmethod
@cached(cache_type='fin_cashflow_vbal', key_params=['company_id', 'luna', 'an'])
@cached(cache_type='fin_cashflow_vbal', key_params=['company_id', 'luna', 'an', 'server_id'])
async def get_cashflow_from_vbal(
company_id: int,
luna: int,
an: int
an: int,
server_id: Optional[str] = None
) -> dict:
"""
Calculează datele de Cash Flow direct din VBAL (balanța de verificare).
@@ -642,9 +645,9 @@ class FinancialIndicatorsService:
- incasari_ytd: Încasări YTD (4111+461 TOTCRED)
- plati_ytd: Plăți YTD (401+404+462 TOTDEB)
"""
schema = await FinancialIndicatorsService._get_schema(company_id)
schema = await FinancialIndicatorsService._get_schema(company_id, server_id)
async with oracle_pool.get_connection() as connection:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
query = f"""
SELECT
@@ -737,7 +740,8 @@ class FinancialIndicatorsService:
async def calculate_liquidity_indicators(
company_id: int,
luna: int,
an: int
an: int,
server_id: Optional[str] = None
) -> LiquidityIndicators:
"""
Calculează indicatorii de lichiditate pentru evaluarea capacității
@@ -763,7 +767,7 @@ class FinancialIndicatorsService:
"""
# Obținem agregatele din balanță
aggregates = await FinancialIndicatorsService.get_balance_sheet_aggregates(
company_id, luna, an
company_id, luna, an, server_id
)
# Ensure aggregates is a BalanceSheetAggregates model (cache may return dict)
if isinstance(aggregates, dict):
@@ -906,11 +910,12 @@ class FinancialIndicatorsService:
return result
@staticmethod
@cached(cache_type='fin_efficiency', key_params=['company_id', 'luna', 'an'])
@cached(cache_type='fin_efficiency', key_params=['company_id', 'luna', 'an', 'server_id'])
async def calculate_efficiency_indicators(
company_id: int,
luna: int,
an: int
an: int,
server_id: Optional[str] = None
) -> EfficiencyIndicators:
"""
Calculează indicatorii de eficiență pentru evaluarea vitezei de conversie
@@ -944,7 +949,8 @@ class FinancialIndicatorsService:
company=str(company_id),
username="system", # System call for indicators
luna=luna,
an=an
an=an,
server_id=server_id
)
# Ensure summary is a DashboardSummary model (cache may return dict)
if isinstance(summary, dict):
@@ -953,7 +959,8 @@ class FinancialIndicatorsService:
# Obținem datele din trends (facturări/încasări/achiziții/plăți lunare)
trends = await DashboardService.get_trends(
company_id=company_id,
period='12m' # Ultimele 12 luni pentru media lunară
period='12m', # Ultimele 12 luni pentru media lunară
server_id=server_id
)
# Extragem soldurile din summary
@@ -1162,11 +1169,12 @@ class FinancialIndicatorsService:
return result
@staticmethod
@cached(cache_type='fin_risk', key_params=['company_id', 'luna', 'an'])
@cached(cache_type='fin_risk', key_params=['company_id', 'luna', 'an', 'server_id'])
async def calculate_risk_indicators(
company_id: int,
luna: int,
an: int
an: int,
server_id: Optional[str] = None
) -> RiskIndicators:
"""
Calculează indicatorii de risc și aging pentru evaluarea sănătății
@@ -1205,7 +1213,8 @@ class FinancialIndicatorsService:
company=str(company_id),
username="system", # System call for indicators
luna=luna,
an=an
an=an,
server_id=server_id
)
# Ensure summary is a DashboardSummary model (cache may return dict)
if isinstance(summary, dict):
@@ -1384,11 +1393,12 @@ class FinancialIndicatorsService:
return result
@staticmethod
@cached(cache_type='fin_cashflow', key_params=['company_id', 'luna', 'an'])
@cached(cache_type='fin_cashflow', key_params=['company_id', 'luna', 'an', 'server_id'])
async def calculate_cashflow_indicators(
company_id: int,
luna: int,
an: int
an: int,
server_id: Optional[str] = None
) -> CashFlowIndicators:
"""
Calculează indicatorii de cash flow pentru evaluarea generării și
@@ -1421,10 +1431,10 @@ class FinancialIndicatorsService:
# Obținem datele de cash flow din VBAL (sursa preferată)
# VBAL oferă date directe: RULCRED/RULDEB pentru lunar, TOTCRED/TOTDEB pentru YTD
cf_data_curent = await FinancialIndicatorsService.get_cashflow_from_vbal(
company_id, luna, an
company_id, luna, an, server_id
)
cf_data_anterior = await FinancialIndicatorsService.get_cashflow_from_vbal(
company_id, luna, an - 1
company_id, luna, an - 1, server_id
)
# Obținem datele din summary pentru datorii restante
@@ -1432,7 +1442,8 @@ class FinancialIndicatorsService:
company=str(company_id),
username="system",
luna=luna,
an=an
an=an,
server_id=server_id
)
# Ensure summary is a DashboardSummary model (cache may return dict)
if isinstance(summary, dict):
@@ -1609,11 +1620,12 @@ class FinancialIndicatorsService:
return result
@staticmethod
@cached(cache_type='fin_dynamics', key_params=['company_id', 'luna', 'an'])
@cached(cache_type='fin_dynamics', key_params=['company_id', 'luna', 'an', 'server_id'])
async def calculate_dynamics_indicators(
company_id: int,
luna: int,
an: int
an: int,
server_id: Optional[str] = None
) -> DynamicsIndicators:
"""
Calculează indicatorii de dinamică pentru evaluarea evoluției afacerii.
@@ -1653,10 +1665,10 @@ class FinancialIndicatorsService:
# Obținem agregatele pentru anul curent și anul anterior
# Cifra de Afaceri (70x) din VBAL - FĂRĂ TVA
aggregates_curent = await FinancialIndicatorsService.get_balance_sheet_aggregates(
company_id, luna, an
company_id, luna, an, server_id
)
aggregates_anterior = await FinancialIndicatorsService.get_balance_sheet_aggregates(
company_id, luna, an - 1
company_id, luna, an - 1, server_id
)
# Ensure aggregates is a BalanceSheetAggregates model (cache may return dict)
@@ -1674,10 +1686,10 @@ class FinancialIndicatorsService:
# Include: 3x=4x (stocuri) + 6x=4x (servicii, consumabile)
# Exclude: discount/rabat (40x=667/609)
achizitii_curent = await FinancialIndicatorsService.get_achizitii_ytd(
company_id, luna, an
company_id, luna, an, server_id
)
achizitii_anterior = await FinancialIndicatorsService.get_achizitii_ytd(
company_id, luna, an - 1
company_id, luna, an - 1, server_id
)
total_achizitii_curent = float(achizitii_curent)
total_achizitii_anterior = float(achizitii_anterior)
@@ -1843,11 +1855,12 @@ class FinancialIndicatorsService:
return result
@staticmethod
@cached(cache_type='fin_altman', key_params=['company_id', 'luna', 'an'])
@cached(cache_type='fin_altman', key_params=['company_id', 'luna', 'an', 'server_id'])
async def calculate_altman_zscore(
company_id: int,
luna: int,
an: int
an: int,
server_id: Optional[str] = None
) -> AltmanZScore:
"""
Calculează Altman Z-Score pentru evaluarea riscului de faliment.
@@ -1880,7 +1893,7 @@ class FinancialIndicatorsService:
"""
# Obținem agregatele din balanță
aggregates = await FinancialIndicatorsService.get_balance_sheet_aggregates(
company_id, luna, an
company_id, luna, an, server_id
)
# Ensure aggregates is a BalanceSheetAggregates model (cache may return dict)
if isinstance(aggregates, dict):
@@ -2088,11 +2101,12 @@ class FinancialIndicatorsService:
return result
@staticmethod
@cached(cache_type='fin_profitability', key_params=['company_id', 'luna', 'an'])
@cached(cache_type='fin_profitability', key_params=['company_id', 'luna', 'an', 'server_id'])
async def calculate_profitability_indicators(
company_id: int,
luna: int,
an: int
an: int,
server_id: Optional[str] = None
) -> ProfitabilityIndicators:
"""
Calculează indicatorii de profitabilitate pentru evaluarea randamentului afacerii.
@@ -2120,7 +2134,7 @@ class FinancialIndicatorsService:
"""
# Obținem agregatele din balanță (include venituri, cheltuieli, active, capital)
aggregates = await FinancialIndicatorsService.get_balance_sheet_aggregates(
company_id, luna, an
company_id, luna, an, server_id
)
# Ensure aggregates is a BalanceSheetAggregates model (cache may return dict)
if isinstance(aggregates, dict):
@@ -2356,7 +2370,8 @@ class FinancialIndicatorsService:
async def calculate_solvability_indicators(
company_id: int,
luna: int,
an: int
an: int,
server_id: Optional[str] = None
) -> SolvabilityIndicators:
"""
Calculează indicatorii de solvabilitate pentru evaluarea capacității
@@ -2384,7 +2399,7 @@ class FinancialIndicatorsService:
"""
# Obținem agregatele din balanță
aggregates = await FinancialIndicatorsService.get_balance_sheet_aggregates(
company_id, luna, an
company_id, luna, an, server_id
)
# Ensure aggregates is a BalanceSheetAggregates model (cache may return dict)
if isinstance(aggregates, dict):
@@ -2555,12 +2570,13 @@ class FinancialIndicatorsService:
return periods
@staticmethod
@cached(cache_type='financial_indicators_historical', ttl=3600, key_params=['company_id', 'months', 'luna', 'an'])
@cached(cache_type='financial_indicators_historical', ttl=3600, key_params=['company_id', 'months', 'luna', 'an', 'server_id'])
async def get_historical_indicators(
company_id: int,
months: int = 12,
luna: Optional[int] = None,
an: Optional[int] = None
an: Optional[int] = None,
server_id: Optional[str] = None
) -> Dict[str, Any]:
"""
Calculează indicatorii financiari pentru ultimele `months` luni
@@ -2672,7 +2688,7 @@ class FinancialIndicatorsService:
try:
# Lichiditate
lichiditate = await FinancialIndicatorsService.calculate_liquidity_indicators(
company_id, period_luna, period_an
company_id, period_luna, period_an, server_id
)
# Ensure lichiditate is a model (cache may return dict)
if isinstance(lichiditate, dict):
@@ -2690,7 +2706,7 @@ class FinancialIndicatorsService:
# Eficiență
eficienta = await FinancialIndicatorsService.calculate_efficiency_indicators(
company_id, period_luna, period_an
company_id, period_luna, period_an, server_id
)
# Ensure eficienta is a model (cache may return dict)
if isinstance(eficienta, dict):
@@ -2706,7 +2722,7 @@ class FinancialIndicatorsService:
# Risc
risc = await FinancialIndicatorsService.calculate_risk_indicators(
company_id, period_luna, period_an
company_id, period_luna, period_an, server_id
)
# Ensure risc is a model (cache may return dict)
if isinstance(risc, dict):
@@ -2725,7 +2741,7 @@ class FinancialIndicatorsService:
# Cash Flow
cash_flow = await FinancialIndicatorsService.calculate_cashflow_indicators(
company_id, period_luna, period_an
company_id, period_luna, period_an, server_id
)
# Ensure cash_flow is a model (cache may return dict)
if isinstance(cash_flow, dict):
@@ -2742,7 +2758,7 @@ class FinancialIndicatorsService:
# Dinamica
dinamica = await FinancialIndicatorsService.calculate_dynamics_indicators(
company_id, period_luna, period_an
company_id, period_luna, period_an, server_id
)
# Ensure dinamica is a model (cache may return dict)
if isinstance(dinamica, dict):
@@ -2758,7 +2774,7 @@ class FinancialIndicatorsService:
# Altman Z-Score
altman = await FinancialIndicatorsService.calculate_altman_zscore(
company_id, period_luna, period_an
company_id, period_luna, period_an, server_id
)
# Ensure altman is a model (cache may return dict)
if isinstance(altman, dict):
@@ -2772,7 +2788,7 @@ class FinancialIndicatorsService:
# Profitabilitate
profitabilitate = await FinancialIndicatorsService.calculate_profitability_indicators(
company_id, period_luna, period_an
company_id, period_luna, period_an, server_id
)
# Ensure profitabilitate is a model (cache may return dict)
if isinstance(profitabilitate, dict):
@@ -2795,7 +2811,7 @@ class FinancialIndicatorsService:
# Solvabilitate
solvabilitate = await FinancialIndicatorsService.calculate_solvability_indicators(
company_id, period_luna, period_an
company_id, period_luna, period_an, server_id
)
# Ensure solvabilitate is a model (cache may return dict)
if isinstance(solvabilitate, dict):
@@ -2829,13 +2845,14 @@ class FinancialIndicatorsService:
return historical_data
@staticmethod
@cached(cache_type='financial_indicators_sparklines', key_params=['company_id', 'luna', 'an', 'months'])
@cached(cache_type='financial_indicators_sparklines', key_params=['company_id', 'luna', 'an', 'months', 'server_id'])
async def get_indicators_with_sparklines(
company_id: int,
luna: int,
an: int,
months: int = 12,
request: Optional[Request] = None
request: Optional[Request] = None,
server_id: Optional[str] = None
) -> FinancialIndicatorsResponse:
"""
Calculează toți indicatorii financiari și adaugă datele de sparkline
@@ -2858,32 +2875,32 @@ class FinancialIndicatorsService:
# Obținem datele istorice și indicatorii curenți în paralel
historical_task = FinancialIndicatorsService.get_historical_indicators(
company_id, months, luna, an
company_id, months, luna, an, server_id
)
lichiditate_task = FinancialIndicatorsService.calculate_liquidity_indicators(
company_id, luna, an
company_id, luna, an, server_id
)
eficienta_task = FinancialIndicatorsService.calculate_efficiency_indicators(
company_id, luna, an
company_id, luna, an, server_id
)
risc_task = FinancialIndicatorsService.calculate_risk_indicators(
company_id, luna, an
company_id, luna, an, server_id
)
cash_flow_task = FinancialIndicatorsService.calculate_cashflow_indicators(
company_id, luna, an
company_id, luna, an, server_id
)
dinamica_task = FinancialIndicatorsService.calculate_dynamics_indicators(
company_id, luna, an
company_id, luna, an, server_id
)
altman_task = FinancialIndicatorsService.calculate_altman_zscore(
company_id, luna, an
company_id, luna, an, server_id
)
profitabilitate_task = FinancialIndicatorsService.calculate_profitability_indicators(
company_id, luna, an
company_id, luna, an, server_id
)
solvabilitate_task = FinancialIndicatorsService.calculate_solvability_indicators(
company_id, luna, an
company_id, luna, an, server_id
)
(