From 2384d3b5b42cb17ec2e1aa7d1687126cad5a921a Mon Sep 17 00:00:00 2001 From: uznadeem Date: Mon, 28 Sep 2026 20:19:33 +0500 Subject: [PATCH 1/8] Fix DB Conection, Remove Mock Data and Add Tenant Cache Key --- backend/app/api/v1/dashboard.py | 9 +- backend/app/core/database_pool.py | 73 ++++++++------- backend/app/main.py | 6 ++ backend/app/services/cache.py | 2 +- backend/app/services/reservations.py | 130 ++++++++++++--------------- backend/pyproject.toml | 2 +- backend/requirements.txt | 2 +- docker-compose.yml | 2 +- 8 files changed, 116 insertions(+), 110 deletions(-) diff --git a/backend/app/api/v1/dashboard.py b/backend/app/api/v1/dashboard.py index 1ec352d7e..d237f027a 100644 --- a/backend/app/api/v1/dashboard.py +++ b/backend/app/api/v1/dashboard.py @@ -1,6 +1,7 @@ from fastapi import APIRouter, Depends, HTTPException from typing import Dict, Any from app.services.cache import get_revenue_summary +from app.services.reservations import RevenueServiceError from app.core.auth import authenticate_request as get_current_user router = APIRouter() @@ -13,7 +14,13 @@ async def get_dashboard_summary( tenant_id = getattr(current_user, "tenant_id", "default_tenant") or "default_tenant" - revenue_data = await get_revenue_summary(property_id, tenant_id) + try: + revenue_data = await get_revenue_summary(property_id, tenant_id) + except RevenueServiceError as error: + raise HTTPException( + status_code=503, + detail="Revenue data is temporarily unavailable", + ) from error total_revenue_float = float(revenue_data['total']) diff --git a/backend/app/core/database_pool.py b/backend/app/core/database_pool.py index d638dfcfe..fe56eb608 100644 --- a/backend/app/core/database_pool.py +++ b/backend/app/core/database_pool.py @@ -1,7 +1,12 @@ -import asyncio -from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker -from sqlalchemy.pool import QueuePool import logging + +from sqlalchemy.engine import make_url +from sqlalchemy.ext.asyncio import ( + AsyncSession, + async_sessionmaker, + create_async_engine, +) + from ..config import settings logger = logging.getLogger(__name__) @@ -13,48 +18,48 @@ def __init__(self): async def initialize(self): """Initialize database connection pool""" - 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}" - - self.engine = create_async_engine( - database_url, - poolclass=QueuePool, - pool_size=20, # Number of connections to maintain - max_overflow=30, # Additional connections when needed - pool_pre_ping=True, # Validate connections - pool_recycle=3600, # Recycle connections every hour - echo=False # Set to True for SQL debugging - ) - - self.session_factory = async_sessionmaker( - bind=self.engine, - class_=AsyncSession, - expire_on_commit=False - ) - - logger.info("✅ Database connection pool initialized") - - except Exception as e: - logger.error(f"❌ Database pool initialization failed: {e}") - self.engine = None - self.session_factory = None + if self.engine is not None: + return + + database_url = make_url(settings.database_url).set( + drivername="postgresql+asyncpg" + ) + + self.engine = create_async_engine( + database_url, + pool_size=settings.database_pool_size, + max_overflow=settings.database_max_overflow, + pool_timeout=settings.database_pool_timeout, + pool_pre_ping=True, + pool_recycle=settings.database_pool_recycle, + ) + + self.session_factory = async_sessionmaker( + bind=self.engine, + class_=AsyncSession, + expire_on_commit=False, + ) + + logger.info("✅ Database connection pool initialized") async def close(self): """Close database connections""" - if self.engine: + if self.engine is not None: await self.engine.dispose() + self.engine = None + self.session_factory = None + logger.info("✅ Database connection pool closed") - 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") + if self.session_factory is None: + raise RuntimeError("Database pool is not initialized") return self.session_factory() # Global database pool instance db_pool = DatabasePool() -async def get_db_session() -> AsyncSession: +async def get_db_session(): """Dependency to get database session""" async with db_pool.get_session() as session: yield session diff --git a/backend/app/main.py b/backend/app/main.py index 00734b2fa..7f1a46307 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -15,6 +15,7 @@ import time from app.core.redis_client import redis_client +from app.core.database_pool import db_pool from .api.v1 import ( users_lightning, cities, @@ -90,6 +91,9 @@ async def lifespan(app: FastAPI): # Startup logger.info("Starting up...") + # Initialize the shared PostgreSQL connection pool. + await db_pool.initialize() + # Initialize Supabase connection pool try: from .core.supabase_connection_pool import supabase_pool @@ -127,6 +131,8 @@ async def lifespan(app: FastAPI): await async_processor.shutdown() logger.info("Async processor shutdown completed") + await db_pool.close() + # Close connection pool try: from .core.supabase_connection_pool import supabase_pool diff --git a/backend/app/services/cache.py b/backend/app/services/cache.py index b81474957..672e3f9d6 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 384bd00ab..f7b5c0bf0 100644 --- a/backend/app/services/reservations.py +++ b/backend/app/services/reservations.py @@ -1,6 +1,14 @@ +import logging from datetime import datetime from decimal import Decimal -from typing import Dict, Any, List +from typing import Any, Dict + + +logger = logging.getLogger(__name__) + + +class RevenueServiceError(RuntimeError): + """Raised when revenue data cannot be retrieved from the database.""" async def calculate_monthly_revenue(property_id: str, month: int, year: int, db_session=None) -> Decimal: """ @@ -36,74 +44,54 @@ async def calculate_total_revenue(property_id: str, tenant_id: str) -> Dict[str, Aggregates revenue from database. """ try: - # Import database pool - from app.core.database_pool import DatabasePool - - # Initialize pool if needed - db_pool = DatabasePool() - 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, - COUNT(*) as reservation_count - FROM reservations - WHERE property_id = :property_id AND tenant_id = :tenant_id - GROUP BY property_id - """) - - result = await session.execute(query, { - "property_id": property_id, - "tenant_id": tenant_id - }) - row = result.fetchone() - - if row: - total_revenue = Decimal(str(row.total_revenue)) - return { - "property_id": property_id, - "tenant_id": tenant_id, - "total": str(total_revenue), - "currency": "USD", - "count": row.reservation_count - } - else: - # No reservations found for this property - return { - "property_id": property_id, - "tenant_id": tenant_id, - "total": "0.00", - "currency": "USD", - "count": 0 - } - else: - 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} - } + # Use the application-wide database pool initialized at startup. + from app.core.database_pool import db_pool - 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'] - } + 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, + COUNT(*) as reservation_count + FROM reservations + WHERE property_id = :property_id AND tenant_id = :tenant_id + GROUP BY property_id + """) + + result = await session.execute(query, { + "property_id": property_id, + "tenant_id": tenant_id + }) + row = result.fetchone() + + if row: + total_revenue = Decimal(str(row.total_revenue)) + return { + "property_id": property_id, + "tenant_id": tenant_id, + "total": str(total_revenue), + "currency": "USD", + "count": row.reservation_count + } + + # No reservations found for this property + return { + "property_id": property_id, + "tenant_id": tenant_id, + "total": "0.00", + "currency": "USD", + "count": 0 + } + + except Exception as error: + logger.exception( + "Revenue query failed for tenant=%s property=%s", + tenant_id, + property_id, + ) + raise RevenueServiceError( + "Revenue data could not be retrieved" + ) from error diff --git a/backend/pyproject.toml b/backend/pyproject.toml index ebaa4bc92..6968736a4 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ "reportlab>=4.4.3", "requests>=2.32.4", "sendgrid>=6.12.4", - "sqlalchemy>=2.0.0", + "sqlalchemy[asyncio]>=2.0.0", "stripe>=7.0.0", "supabase>=2.18.0", "tenacity>=9.1.2", diff --git a/backend/requirements.txt b/backend/requirements.txt index 6b777d2aa..06cb8bd4a 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -18,7 +18,7 @@ pytz redis>=5.0.0 requests sendgrid -sqlalchemy>=2.0.0 +sqlalchemy[asyncio]>=2.0.0 supabase tenacity>=8.0.0 uvicorn[standard] diff --git a/docker-compose.yml b/docker-compose.yml index e255de0b5..dc1eacfe6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,7 +35,7 @@ services: - POSTGRES_PASSWORD=postgres - POSTGRES_DB=propertyflow ports: - - "5433:5432" + - "5438:5432" volumes: # Initialize DB with schema and seeds - ./database/schema.sql:/docker-entrypoint-initdb.d/1-schema.sql From ba19a4a89cf59051432b0f6f886c9a9487ccd603 Mon Sep 17 00:00:00 2001 From: uznadeem Date: Mon, 28 Sep 2026 20:24:26 +0500 Subject: [PATCH 2/8] Added tenant-scoped property ownership query --- backend/app/api/v1/dashboard.py | 7 ++++++- backend/app/services/reservations.py | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/backend/app/api/v1/dashboard.py b/backend/app/api/v1/dashboard.py index d237f027a..d4e0207c5 100644 --- a/backend/app/api/v1/dashboard.py +++ b/backend/app/api/v1/dashboard.py @@ -1,7 +1,7 @@ from fastapi import APIRouter, Depends, HTTPException from typing import Dict, Any from app.services.cache import get_revenue_summary -from app.services.reservations import RevenueServiceError +from app.services.reservations import PropertyNotFoundError, RevenueServiceError from app.core.auth import authenticate_request as get_current_user router = APIRouter() @@ -16,6 +16,11 @@ async def get_dashboard_summary( try: revenue_data = await get_revenue_summary(property_id, tenant_id) + except PropertyNotFoundError as error: + raise HTTPException( + status_code=404, + detail="Property not found", + ) from error except RevenueServiceError as error: raise HTTPException( status_code=503, diff --git a/backend/app/services/reservations.py b/backend/app/services/reservations.py index f7b5c0bf0..3b7640de7 100644 --- a/backend/app/services/reservations.py +++ b/backend/app/services/reservations.py @@ -10,6 +10,11 @@ class RevenueServiceError(RuntimeError): """Raised when revenue data cannot be retrieved from the database.""" + +class PropertyNotFoundError(LookupError): + """Raised when a property does not belong to the requesting tenant.""" + + async def calculate_monthly_revenue(property_id: str, month: int, year: int, db_session=None) -> Decimal: """ Calculates revenue for a specific month. @@ -51,6 +56,20 @@ async def calculate_total_revenue(property_id: str, tenant_id: str) -> Dict[str, # Use SQLAlchemy text for raw SQL from sqlalchemy import text + property_query = text(""" + SELECT timezone + FROM properties + WHERE id = :property_id AND tenant_id = :tenant_id + LIMIT 1 + """) + property_result = await session.execute(property_query, { + "property_id": property_id, + "tenant_id": tenant_id + }) + + if property_result.scalar_one_or_none() is None: + raise PropertyNotFoundError("Property not found") + query = text(""" SELECT property_id, @@ -86,6 +105,8 @@ async def calculate_total_revenue(property_id: str, tenant_id: str) -> Dict[str, "count": 0 } + except PropertyNotFoundError: + raise except Exception as error: logger.exception( "Revenue query failed for tenant=%s property=%s", From 0d64ef179d7f213a057104eb91e27f5c63e50fbd Mon Sep 17 00:00:00 2001 From: uznadeem Date: Mon, 28 Sep 2026 20:30:09 +0500 Subject: [PATCH 3/8] fix: correct tenant-isolated monthly revenue calculation --- backend/app/api/v1/dashboard.py | 11 ++++- backend/app/services/cache.py | 13 ++++-- backend/app/services/reservations.py | 65 +++++++++++++--------------- 3 files changed, 47 insertions(+), 42 deletions(-) diff --git a/backend/app/api/v1/dashboard.py b/backend/app/api/v1/dashboard.py index d4e0207c5..79081d81b 100644 --- a/backend/app/api/v1/dashboard.py +++ b/backend/app/api/v1/dashboard.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, Query from typing import Dict, Any from app.services.cache import get_revenue_summary from app.services.reservations import PropertyNotFoundError, RevenueServiceError @@ -9,13 +9,20 @@ @router.get("/dashboard/summary") async def get_dashboard_summary( property_id: str, + year: int = Query(ge=1, le=9998), + month: int = Query(ge=1, le=12), current_user: dict = Depends(get_current_user) ) -> Dict[str, Any]: tenant_id = getattr(current_user, "tenant_id", "default_tenant") or "default_tenant" try: - revenue_data = await get_revenue_summary(property_id, tenant_id) + revenue_data = await get_revenue_summary( + property_id, + tenant_id, + year, + month, + ) except PropertyNotFoundError as error: raise HTTPException( status_code=404, diff --git a/backend/app/services/cache.py b/backend/app/services/cache.py index 672e3f9d6..5f73dd7f5 100644 --- a/backend/app/services/cache.py +++ b/backend/app/services/cache.py @@ -6,11 +6,16 @@ # Initialize Redis client (typically configured centrally). redis_client = redis.Redis.from_url(os.getenv("REDIS_URL", "redis://localhost:6379/0")) -async def get_revenue_summary(property_id: str, tenant_id: str) -> Dict[str, Any]: +async def get_revenue_summary( + property_id: str, + tenant_id: str, + year: int, + month: int, +) -> Dict[str, Any]: """ Fetches revenue summary, utilizing caching to improve performance. """ - cache_key = f"revenue:{tenant_id}:{property_id}" + cache_key = f"revenue:{tenant_id}:{property_id}:{year}:{month}" # Try to get from cache cached = await redis_client.get(cache_key) @@ -18,10 +23,10 @@ async def get_revenue_summary(property_id: str, tenant_id: str) -> Dict[str, Any return json.loads(cached) # Revenue calculation is delegated to the reservation service. - from app.services.reservations import calculate_total_revenue + from app.services.reservations import calculate_monthly_revenue # Calculate revenue - result = await calculate_total_revenue(property_id, tenant_id) + result = await calculate_monthly_revenue(property_id, tenant_id, year, month) # Cache the result for 5 minutes await redis_client.setex(cache_key, 300, json.dumps(result)) diff --git a/backend/app/services/reservations.py b/backend/app/services/reservations.py index 3b7640de7..20f1afe3b 100644 --- a/backend/app/services/reservations.py +++ b/backend/app/services/reservations.py @@ -1,7 +1,8 @@ import logging -from datetime import datetime +from datetime import datetime, timezone from decimal import Decimal from typing import Any, Dict +from zoneinfo import ZoneInfo logger = logging.getLogger(__name__) @@ -15,36 +16,12 @@ class PropertyNotFoundError(LookupError): """Raised when a property does not belong to the requesting tenant.""" -async def calculate_monthly_revenue(property_id: str, month: int, year: int, db_session=None) -> Decimal: - """ - Calculates revenue for a specific month. - """ - - start_date = datetime(year, month, 1) - if month < 12: - end_date = datetime(year, month + 1, 1) - else: - end_date = datetime(year + 1, 1, 1) - - print(f"DEBUG: Querying revenue for {property_id} from {start_date} to {end_date}") - - # SQL Simulation (This would be executed against the actual DB) - query = """ - SELECT SUM(total_amount) as total - FROM reservations - WHERE property_id = $1 - AND tenant_id = $2 - AND check_in_date >= $3 - AND check_in_date < $4 - """ - - # In production this query executes against a database session. - # result = await db.fetch_val(query, property_id, tenant_id, start_date, end_date) - # return result or Decimal('0') - - 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]: +async def calculate_monthly_revenue( + property_id: str, + tenant_id: str, + year: int, + month: int, +) -> Dict[str, Any]: """ Aggregates revenue from database. """ @@ -67,22 +44,38 @@ async def calculate_total_revenue(property_id: str, tenant_id: str) -> Dict[str, "tenant_id": tenant_id }) - if property_result.scalar_one_or_none() is None: + property_timezone = property_result.scalar_one_or_none() + if property_timezone is None: raise PropertyNotFoundError("Property not found") + local_timezone = ZoneInfo(property_timezone) + start_local = datetime(year, month, 1, tzinfo=local_timezone) + if month == 12: + end_local = datetime(year + 1, 1, 1, tzinfo=local_timezone) + else: + end_local = datetime(year, month + 1, 1, tzinfo=local_timezone) + + start_utc = start_local.astimezone(timezone.utc) + end_utc = end_local.astimezone(timezone.utc) + query = text(""" SELECT property_id, - SUM(total_amount) as total_revenue, - COUNT(*) as reservation_count + SUM(total_amount) AS total_revenue, + COUNT(*) AS reservation_count FROM reservations - WHERE property_id = :property_id AND tenant_id = :tenant_id + WHERE property_id = :property_id + AND tenant_id = :tenant_id + AND check_in_date >= :start_utc + AND check_in_date < :end_utc GROUP BY property_id """) result = await session.execute(query, { "property_id": property_id, - "tenant_id": tenant_id + "tenant_id": tenant_id, + "start_utc": start_utc, + "end_utc": end_utc }) row = result.fetchone() From b18822a47fa61127def0327c1bf7f863630e4727 Mon Sep 17 00:00:00 2001 From: uznadeem Date: Mon, 28 Sep 2026 20:34:35 +0500 Subject: [PATCH 4/8] Add monthly revenue filters to dashboard --- frontend/src/components/Dashboard.tsx | 58 ++++++++++++++++------ frontend/src/components/RevenueSummary.tsx | 18 +++---- frontend/src/lib/secureApi.ts | 23 +++------ 3 files changed, 56 insertions(+), 43 deletions(-) diff --git a/frontend/src/components/Dashboard.tsx b/frontend/src/components/Dashboard.tsx index a21bba404..cda656bfb 100644 --- a/frontend/src/components/Dashboard.tsx +++ b/frontend/src/components/Dashboard.tsx @@ -11,6 +11,8 @@ const PROPERTIES = [ const Dashboard: React.FC = () => { const [selectedProperty, setSelectedProperty] = useState('prop-001'); + const [month, setMonth] = useState(3); + const [year, setYear] = useState(2024); return (
@@ -27,26 +29,52 @@ const Dashboard: React.FC = () => {

- {/* Property Selector */} -
- - +
+
+ + +
+ +
+ + +
+ +
+ + setYear(Number(e.target.value))} + className="w-24 px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 text-sm" + /> +
- +
diff --git a/frontend/src/components/RevenueSummary.tsx b/frontend/src/components/RevenueSummary.tsx index dbb6d0629..2f8a63eb4 100644 --- a/frontend/src/components/RevenueSummary.tsx +++ b/frontend/src/components/RevenueSummary.tsx @@ -9,28 +9,22 @@ interface RevenueData { } interface RevenueSummaryProps { - propertyId?: string; - debugTenant?: string; + propertyId: string; + year: number; + month: number; showRaw?: boolean; } -export const RevenueSummary: React.FC = ({ propertyId = 'prop-001', debugTenant, showRaw }) => { +export const RevenueSummary: React.FC = ({ propertyId, year, month, 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); 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, year, month); setData(response); } catch (err) { setError('Failed to load revenue data'); @@ -41,7 +35,7 @@ export const RevenueSummary: React.FC = ({ propertyId = 'pr }; fetchRevenue(); - }, [propertyId, activeTenant]); + }, [propertyId, year, month]); if (loading) { return ( diff --git a/frontend/src/lib/secureApi.ts b/frontend/src/lib/secureApi.ts index f85f04c90..a3d65de37 100644 --- a/frontend/src/lib/secureApi.ts +++ b/frontend/src/lib/secureApi.ts @@ -1449,23 +1449,14 @@ export class SecureAPIClient { } // ============= DASHBOARD API ============= - /** - * Get dashboard summary with optional simulation header - */ - async getDashboardSummary(propertyId: string, options?: { simulatedTenant?: string, timestamp?: number }) { - 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 - }; - } + async getDashboardSummary(propertyId: string, year: number, month: number) { + const queryParams = new URLSearchParams({ + property_id: propertyId, + year: year.toString(), + month: month.toString() + }); - return this.request(`/api/v1/dashboard/summary?${queryParams}`, requestOptions); + return this.request(`/api/v1/dashboard/summary?${queryParams}`); } async uploadCompanyLogo(logo_url: string) { From 305b98cc9a722a2c0e8e650e69e62bcab4cd998c Mon Sep 17 00:00:00 2001 From: uznadeem Date: Mon, 28 Sep 2026 20:39:23 +0500 Subject: [PATCH 5/8] Preserve decimal precision in revenue totals --- backend/app/api/v1/dashboard.py | 4 +--- backend/app/services/reservations.py | 9 ++++++--- frontend/src/components/RevenueSummary.tsx | 17 ++--------------- 3 files changed, 9 insertions(+), 21 deletions(-) diff --git a/backend/app/api/v1/dashboard.py b/backend/app/api/v1/dashboard.py index 79081d81b..278a39405 100644 --- a/backend/app/api/v1/dashboard.py +++ b/backend/app/api/v1/dashboard.py @@ -34,11 +34,9 @@ async def get_dashboard_summary( detail="Revenue data is temporarily unavailable", ) from error - total_revenue_float = float(revenue_data['total']) - return { "property_id": revenue_data['property_id'], - "total_revenue": total_revenue_float, + "total_revenue": revenue_data['total'], "currency": revenue_data['currency'], "reservations_count": revenue_data['count'] } diff --git a/backend/app/services/reservations.py b/backend/app/services/reservations.py index 20f1afe3b..9191d00cf 100644 --- a/backend/app/services/reservations.py +++ b/backend/app/services/reservations.py @@ -1,6 +1,6 @@ import logging from datetime import datetime, timezone -from decimal import Decimal +from decimal import Decimal, ROUND_HALF_UP from typing import Any, Dict from zoneinfo import ZoneInfo @@ -80,11 +80,14 @@ async def calculate_monthly_revenue( row = result.fetchone() if row: - total_revenue = Decimal(str(row.total_revenue)) + total_revenue = Decimal(str(row.total_revenue)).quantize( + Decimal("0.01"), + rounding=ROUND_HALF_UP, + ) return { "property_id": property_id, "tenant_id": tenant_id, - "total": str(total_revenue), + "total": format(total_revenue, ".2f"), "currency": "USD", "count": row.reservation_count } diff --git a/frontend/src/components/RevenueSummary.tsx b/frontend/src/components/RevenueSummary.tsx index 2f8a63eb4..e5fada046 100644 --- a/frontend/src/components/RevenueSummary.tsx +++ b/frontend/src/components/RevenueSummary.tsx @@ -3,7 +3,7 @@ import { SecureAPI } from '../lib/secureApi'; interface RevenueData { property_id: string; - total_revenue: number; + total_revenue: string; currency: string; reservations_count: number; } @@ -55,8 +55,6 @@ export const RevenueSummary: React.FC = ({ propertyId, year if (error) return
{error}
; if (!data) return null; - const displayTotal = Math.round(data.total_revenue * 100) / 100; - return (
{showRaw && ( @@ -72,7 +70,7 @@ export const RevenueSummary: React.FC = ({ propertyId, year

Total Revenue

- {data.currency} {displayTotal.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} + {data.currency} {data.total_revenue} {/* Fake trend indicator for premium feel */} @@ -96,17 +94,6 @@ export const RevenueSummary: React.FC = ({ propertyId, year
- {/* Precision Warning Area */} -
- {Math.abs(data.total_revenue - displayTotal) > 0.000001 && showRaw && ( -
- - - - Precision Mismatch Detected -
- )} -
); From b2d20602654298ed31fa83318d4912dc744da51c Mon Sep 17 00:00:00 2001 From: uznadeem Date: Mon, 28 Sep 2026 20:43:11 +0500 Subject: [PATCH 6/8] Scope dashboard properties by tenant --- backend/app/api/v1/dashboard.py | 30 ++++++++++++++++++++++- frontend/src/components/Dashboard.tsx | 35 ++++++++++++++++++--------- frontend/src/lib/secureApi.ts | 4 +++ 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/backend/app/api/v1/dashboard.py b/backend/app/api/v1/dashboard.py index 278a39405..c44b44df8 100644 --- a/backend/app/api/v1/dashboard.py +++ b/backend/app/api/v1/dashboard.py @@ -1,11 +1,39 @@ from fastapi import APIRouter, Depends, HTTPException, Query -from typing import Dict, Any +from typing import Any, Dict, List +from sqlalchemy import text from app.services.cache import get_revenue_summary from app.services.reservations import PropertyNotFoundError, RevenueServiceError from app.core.auth import authenticate_request as get_current_user +from app.core.database_pool import db_pool router = APIRouter() + +@router.get("/dashboard/properties") +async def get_dashboard_properties( + current_user: dict = Depends(get_current_user) +) -> List[Dict[str, str]]: + tenant_id = getattr(current_user, "tenant_id", "default_tenant") or "default_tenant" + + try: + async with db_pool.get_session() as session: + result = await session.execute(text(""" + SELECT id, name + FROM properties + WHERE tenant_id = :tenant_id + ORDER BY name + """), {"tenant_id": tenant_id}) + return [ + {"id": row.id, "name": row.name} + for row in result.fetchall() + ] + except Exception as error: + raise HTTPException( + status_code=503, + detail="Properties are temporarily unavailable", + ) from error + + @router.get("/dashboard/summary") async def get_dashboard_summary( property_id: str, diff --git a/frontend/src/components/Dashboard.tsx b/frontend/src/components/Dashboard.tsx index cda656bfb..67b803a66 100644 --- a/frontend/src/components/Dashboard.tsx +++ b/frontend/src/components/Dashboard.tsx @@ -1,19 +1,28 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { RevenueSummary } from "./RevenueSummary"; +import { SecureAPI } from "../lib/secureApi"; -const PROPERTIES = [ - { id: 'prop-001', name: 'Beach House Alpha' }, - { id: 'prop-002', name: 'City Apartment Downtown' }, - { id: 'prop-003', name: 'Country Villa Estate' }, - { id: 'prop-004', name: 'Lakeside Cottage' }, - { id: 'prop-005', name: 'Urban Loft Modern' } -]; +interface Property { + id: string; + name: string; +} const Dashboard: React.FC = () => { - const [selectedProperty, setSelectedProperty] = useState('prop-001'); + const [properties, setProperties] = useState(null); + const [selectedProperty, setSelectedProperty] = useState(''); + const [propertiesError, setPropertiesError] = useState(''); const [month, setMonth] = useState(3); const [year, setYear] = useState(2024); + useEffect(() => { + SecureAPI.getDashboardProperties() + .then((result: Property[]) => { + setProperties(result); + setSelectedProperty(result[0]?.id || ''); + }) + .catch(() => setPropertiesError('Failed to load properties')); + }, []); + return (
@@ -37,7 +46,7 @@ const Dashboard: React.FC = () => { onChange={(e) => setSelectedProperty(e.target.value)} className="block min-w-[200px] px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 text-sm" > - {PROPERTIES.map((property) => ( + {properties?.map((property) => ( @@ -74,7 +83,11 @@ const Dashboard: React.FC = () => {
- + {propertiesError &&
{propertiesError}
} + {!propertiesError && properties?.length === 0 &&
No properties available.
} + {selectedProperty && ( + + )}
diff --git a/frontend/src/lib/secureApi.ts b/frontend/src/lib/secureApi.ts index a3d65de37..3c2cd8374 100644 --- a/frontend/src/lib/secureApi.ts +++ b/frontend/src/lib/secureApi.ts @@ -1449,6 +1449,10 @@ export class SecureAPIClient { } // ============= DASHBOARD API ============= + async getDashboardProperties() { + return this.request('/api/v1/dashboard/properties'); + } + async getDashboardSummary(propertyId: string, year: number, month: number) { const queryParams = new URLSearchParams({ property_id: propertyId, From af4ff8e301e8569c20ee88078d2b5b0f336c6f75 Mon Sep 17 00:00:00 2001 From: uznadeem Date: Mon, 28 Sep 2026 20:45:07 +0500 Subject: [PATCH 7/8] Exclude non-USD reservations from revenue totals --- backend/app/services/reservations.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/app/services/reservations.py b/backend/app/services/reservations.py index 9191d00cf..9577eca0d 100644 --- a/backend/app/services/reservations.py +++ b/backend/app/services/reservations.py @@ -66,6 +66,7 @@ async def calculate_monthly_revenue( FROM reservations WHERE property_id = :property_id AND tenant_id = :tenant_id + AND currency = 'USD' AND check_in_date >= :start_utc AND check_in_date < :end_utc GROUP BY property_id From 8bf79e34e2744ca56fb85dc936306e2668cf7bf3 Mon Sep 17 00:00:00 2001 From: uznadeem Date: Mon, 28 Sep 2026 21:00:05 +0500 Subject: [PATCH 8/8] Revert DB Docker Port --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index dc1eacfe6..07ef9e1ce 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,7 +35,7 @@ services: - POSTGRES_PASSWORD=postgres - POSTGRES_DB=propertyflow ports: - - "5438:5432" + - "5432:5432" volumes: # Initialize DB with schema and seeds - ./database/schema.sql:/docker-entrypoint-initdb.d/1-schema.sql