Files
roa2web-service-auto/shared/routes/calendar.py
Claude Agent b137e80b71 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>
2026-01-26 22:39:06 +00:00

152 lines
5.6 KiB
Python

"""
Shared Calendar Router Factory for ROA2WEB Applications
Creates a FastAPI router for /api/calendar endpoints that can be used
by both the unified monolith backend.
Usage:
from shared.routes.calendar import create_calendar_router
calendar_router = create_calendar_router(oracle_pool, cache_decorator=cached)
app.include_router(calendar_router, prefix="/api/calendar")
"""
import logging
from typing import Optional, Callable, List
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from auth.dependencies import get_current_user
from auth.models import CurrentUser
from models.calendar import CalendarPeriod, CalendarPeriodsResponse
logger = logging.getLogger(__name__)
# Romanian month names
MONTH_NAMES_RO = [
"Ianuarie", "Februarie", "Martie", "Aprilie", "Mai", "Iunie",
"Iulie", "August", "Septembrie", "Octombrie", "Noiembrie", "Decembrie"
]
def create_calendar_router(
oracle_pool,
cache_decorator: Optional[Callable] = None,
tags: Optional[List[str]] = None
) -> APIRouter:
"""
Factory function to create a calendar router.
Args:
oracle_pool: The Oracle connection pool instance
cache_decorator: Optional caching decorator (e.g., @cached)
tags: OpenAPI tags for the router
Returns:
Configured FastAPI router for calendar endpoints
"""
router = APIRouter(
redirect_slashes=False,
tags=tags or ["calendar"]
)
# Helper to get schema for company
async def _get_schema_for_company(company_id: int, server_id: Optional[str] = None) -> Optional[str]:
"""Get Oracle schema for company ID.
Args:
company_id: The company ID to get schema for
server_id: The Oracle server ID (for multi-server mode)
"""
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
cursor.execute("""
SELECT SCHEMA FROM CONTAFIN_ORACLE.V_NOM_FIRME
WHERE ID_FIRMA = :company_id
""", {'company_id': company_id})
result = cursor.fetchone()
return result[0] if result else None
# Apply cache to schema lookup if decorator provided
# Include server_id in cache key for multi-server mode
if cache_decorator:
_get_schema_for_company = cache_decorator(
cache_type='schema',
key_params=['company_id', 'server_id']
)(_get_schema_for_company)
# Helper to get periods - can be cached
async def _get_available_periods(company_id: int, server_id: Optional[str] = None) -> CalendarPeriodsResponse:
"""Get available accounting periods for a company.
Args:
company_id: The company ID to get periods for
server_id: The Oracle server ID (for multi-server mode)
"""
schema = await _get_schema_for_company(company_id, server_id)
if not schema:
logger.warning(f"Schema not found for company {company_id}")
return CalendarPeriodsResponse(periods=[], current_period=None, total_count=0)
try:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
cursor.execute(f"""
SELECT ANUL, LUNA
FROM {schema}.CALENDAR
ORDER BY ANUL DESC, LUNA DESC
""")
rows = cursor.fetchall()
periods = []
for row in rows:
an, luna = row[0], row[1]
month_name = MONTH_NAMES_RO[luna - 1]
periods.append(CalendarPeriod(
an=an,
luna=luna,
display_name=f"{month_name} {an}"
))
current_period = periods[0] if periods else None
logger.info(f"Loaded {len(periods)} periods for company {company_id}")
return CalendarPeriodsResponse(
periods=periods,
current_period=current_period,
total_count=len(periods)
)
except Exception as e:
logger.error(f"Error fetching periods for company {company_id}: {e}")
return CalendarPeriodsResponse(periods=[], current_period=None, total_count=0)
# Apply cache decorator if provided
# Include server_id in cache key for multi-server mode
if cache_decorator:
_get_available_periods = cache_decorator(
cache_type='calendar_periods',
key_params=['company_id', 'server_id']
)(_get_available_periods)
@router.get("/periods", response_model=CalendarPeriodsResponse)
async def get_calendar_periods(
request: Request,
company: int = Query(..., description="Company ID"),
current_user: CurrentUser = Depends(get_current_user)
) -> CalendarPeriodsResponse:
"""
Get available accounting periods for a company.
Returns periods ordered by year DESC, month DESC with Romanian month names.
"""
# Validate company access
if str(company) not in current_user.companies:
raise HTTPException(403, f"Nu aveți acces la firma {company}")
# Get server_id from request state (injected by auth middleware from JWT)
server_id = getattr(request.state, 'server_id', None)
return await _get_available_periods(company, server_id)
return router