diff --git a/backend/app/api/v1/dashboard.py b/backend/app/api/v1/dashboard.py index 1ec352d7e3..aa06992950 100644 --- a/backend/app/api/v1/dashboard.py +++ b/backend/app/api/v1/dashboard.py @@ -5,21 +5,28 @@ router = APIRouter() + @router.get("/dashboard/summary") async def get_dashboard_summary( property_id: str, - current_user: dict = Depends(get_current_user) + current_user: Any = Depends(get_current_user) ) -> Dict[str, Any]: - - tenant_id = getattr(current_user, "tenant_id", "default_tenant") or "default_tenant" - - revenue_data = await get_revenue_summary(property_id, tenant_id) - - total_revenue_float = float(revenue_data['total']) - + tenant_id = getattr(current_user, "tenant_id", None) + if not tenant_id: + raise HTTPException(status_code=403, detail="No tenant assigned") + + try: + revenue_data = await get_revenue_summary(property_id, tenant_id) + except HTTPException: + raise + except Exception: + raise HTTPException(status_code=503, detail="Revenue data temporarily unavailable") + return { "property_id": revenue_data['property_id'], - "total_revenue": total_revenue_float, + "total_revenue": float(revenue_data['total']), "currency": revenue_data['currency'], + "tenant_id": tenant_id, "reservations_count": revenue_data['count'] } + diff --git a/backend/app/core/database_pool.py b/backend/app/core/database_pool.py index d638dfcfe3..847dd5e4c9 100644 --- a/backend/app/core/database_pool.py +++ b/backend/app/core/database_pool.py @@ -1,6 +1,6 @@ import asyncio from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker -from sqlalchemy.pool import QueuePool +from sqlalchemy.engine import make_url import logging from ..config import settings @@ -13,13 +13,13 @@ def __init__(self): async def initialize(self): """Initialize database connection pool""" + if self.engine and self.session_factory: + return try: - # Create async engine with connection pooling - database_url = f"postgresql+asyncpg://{settings.supabase_db_user}:{settings.supabase_db_password}@{settings.supabase_db_host}:{settings.supabase_db_port}/{settings.supabase_db_name}" - + db_url = make_url(settings.database_url).set(drivername="postgresql+asyncpg") + self.engine = create_async_engine( - database_url, - poolclass=QueuePool, + db_url, pool_size=20, # Number of connections to maintain max_overflow=30, # Additional connections when needed pool_pre_ping=True, # Validate connections @@ -45,7 +45,7 @@ async def close(self): if self.engine: await self.engine.dispose() - async def get_session(self) -> AsyncSession: + def get_session(self) -> AsyncSession: """Get database session from pool""" if not self.session_factory: raise Exception("Database pool not initialized") diff --git a/backend/app/services/cache.py b/backend/app/services/cache.py index b81474957f..672e3f9d69 100644 --- a/backend/app/services/cache.py +++ b/backend/app/services/cache.py @@ -10,7 +10,7 @@ async def get_revenue_summary(property_id: str, tenant_id: str) -> Dict[str, Any """ Fetches revenue summary, utilizing caching to improve performance. """ - cache_key = f"revenue:{property_id}" + cache_key = f"revenue:{tenant_id}:{property_id}" # Try to get from cache cached = await redis_client.get(cache_key) diff --git a/backend/app/services/reservations.py b/backend/app/services/reservations.py index 384bd00abe..8be12c0852 100644 --- a/backend/app/services/reservations.py +++ b/backend/app/services/reservations.py @@ -1,7 +1,10 @@ +import logging from datetime import datetime -from decimal import Decimal +from decimal import Decimal, ROUND_HALF_UP from typing import Dict, Any, List +logger = logging.getLogger(__name__) + async def calculate_monthly_revenue(property_id: str, month: int, year: int, db_session=None) -> Decimal: """ Calculates revenue for a specific month. @@ -31,24 +34,21 @@ async def calculate_monthly_revenue(property_id: str, month: int, year: int, db_ return Decimal('0') # Placeholder for now until DB connection is finalized + async def calculate_total_revenue(property_id: str, tenant_id: str) -> Dict[str, Any]: """ Aggregates revenue from database. """ try: - # Import database pool - from app.core.database_pool import DatabasePool - - # Initialize pool if needed - db_pool = DatabasePool() + from app.core.database_pool import db_pool await db_pool.initialize() if db_pool.session_factory: async with db_pool.get_session() as session: - # Use SQLAlchemy text for raw SQL from sqlalchemy import text query = text(""" + SELECT property_id, SUM(total_amount) as total_revenue, @@ -65,7 +65,7 @@ async def calculate_total_revenue(property_id: str, tenant_id: str) -> Dict[str, row = result.fetchone() if row: - total_revenue = Decimal(str(row.total_revenue)) + total_revenue = Decimal(str(row.total_revenue or 0)).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP) return { "property_id": property_id, "tenant_id": tenant_id, @@ -86,24 +86,5 @@ async def calculate_total_revenue(property_id: str, tenant_id: str) -> Dict[str, raise Exception("Database pool not available") except Exception as e: - print(f"Database error for {property_id} (tenant: {tenant_id}): {e}") - - # Create property-specific mock data for testing when DB is unavailable - # This ensures each property shows different figures - mock_data = { - 'prop-001': {'total': '1000.00', 'count': 3}, - 'prop-002': {'total': '4975.50', 'count': 4}, - 'prop-003': {'total': '6100.50', 'count': 2}, - 'prop-004': {'total': '1776.50', 'count': 4}, - 'prop-005': {'total': '3256.00', 'count': 3} - } - - mock_property_data = mock_data.get(property_id, {'total': '0.00', 'count': 0}) - - return { - "property_id": property_id, - "tenant_id": tenant_id, - "total": mock_property_data['total'], - "currency": "USD", - "count": mock_property_data['count'] - } + logger.error(f"Database error calculating revenue for {property_id} (tenant: {tenant_id}): {e}") + raise diff --git a/backend/requirements.txt b/backend/requirements.txt index 6b777d2aaa..54e5bf39b5 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -19,6 +19,7 @@ redis>=5.0.0 requests sendgrid sqlalchemy>=2.0.0 +greenlet>=3.0.0 supabase tenacity>=8.0.0 uvicorn[standard] diff --git a/frontend/src/components/RevenueSummary.tsx b/frontend/src/components/RevenueSummary.tsx index dbb6d0629d..a2fae071c5 100644 --- a/frontend/src/components/RevenueSummary.tsx +++ b/frontend/src/components/RevenueSummary.tsx @@ -10,27 +10,20 @@ interface RevenueData { interface RevenueSummaryProps { propertyId?: string; - debugTenant?: string; showRaw?: boolean; } -export const RevenueSummary: React.FC = ({ propertyId = 'prop-001', debugTenant, showRaw }) => { +export const RevenueSummary: React.FC = ({ propertyId = 'prop-001', showRaw }) => { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); - const activeTenant = debugTenant || 'candidate'; - useEffect(() => { const fetchRevenue = async () => { setLoading(true); + setError(''); try { - // Use SecureAPI to handle authentication automatically - // We pass the simulatedTenant option which SecureAPI will attach as a header - const response = await SecureAPI.getDashboardSummary(propertyId, { - simulatedTenant: activeTenant, - timestamp: Date.now() - }); + const response = await SecureAPI.getDashboardSummary(propertyId); setData(response); } catch (err) { setError('Failed to load revenue data'); @@ -41,7 +34,8 @@ export const RevenueSummary: React.FC = ({ propertyId = 'pr }; fetchRevenue(); - }, [propertyId, activeTenant]); + }, [propertyId]); + if (loading) { return ( diff --git a/frontend/src/lib/secureApi.ts b/frontend/src/lib/secureApi.ts index f85f04c90a..0d0a7b45f2 100644 --- a/frontend/src/lib/secureApi.ts +++ b/frontend/src/lib/secureApi.ts @@ -1450,24 +1450,14 @@ export class SecureAPIClient { // ============= DASHBOARD API ============= /** - * Get dashboard summary with optional simulation header + * Get dashboard summary */ - async getDashboardSummary(propertyId: string, options?: { simulatedTenant?: string, timestamp?: number }) { + async getDashboardSummary(propertyId: string) { const queryParams = new URLSearchParams({ property_id: propertyId }); - if (options?.timestamp) { - queryParams.append('_t', options.timestamp.toString()); - } - - const requestOptions: RequestInit = {}; - if (options?.simulatedTenant) { - requestOptions.headers = { - 'X-Simulated-Tenant': options.simulatedTenant - }; - } - - return this.request(`/api/v1/dashboard/summary?${queryParams}`, requestOptions); + return this.request(`/api/v1/dashboard/summary?${queryParams}`); } + async uploadCompanyLogo(logo_url: string) { return this.request('/api/v1/company-settings/logo', { method: 'POST',