From 65c53865b4babe51f096d9219300676a953691e5 Mon Sep 17 00:00:00 2001 From: ramazanberkozbek <20217170057@ogr.oku.edu.tr> Date: Fri, 16 Oct 2026 15:23:43 +0300 Subject: [PATCH 01/10] Fix asyncpg connection in DatabasePool to query live database instead of mock data --- backend/app/core/database_pool.py | 11 +++++++---- backend/app/services/reservations.py | 6 +----- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/backend/app/core/database_pool.py b/backend/app/core/database_pool.py index d638dfcfe3..b5bc6a1694 100644 --- a/backend/app/core/database_pool.py +++ b/backend/app/core/database_pool.py @@ -13,12 +13,15 @@ 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 = settings.database_url + if db_url.startswith("postgresql://"): + db_url = db_url.replace("postgresql://", "postgresql+asyncpg://", 1) + self.engine = create_async_engine( - database_url, + db_url, poolclass=QueuePool, pool_size=20, # Number of connections to maintain max_overflow=30, # Additional connections when needed diff --git a/backend/app/services/reservations.py b/backend/app/services/reservations.py index 384bd00abe..bcd936d108 100644 --- a/backend/app/services/reservations.py +++ b/backend/app/services/reservations.py @@ -36,11 +36,7 @@ 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() + from app.core.database_pool import db_pool await db_pool.initialize() if db_pool.session_factory: From 6f0b1f7f9376ba033201a05c14ea56f8c9d2d405 Mon Sep 17 00:00:00 2001 From: ramazanberkozbek <20217170057@ogr.oku.edu.tr> Date: Fri, 16 Oct 2026 15:29:54 +0300 Subject: [PATCH 02/10] Prevent cross-tenant cache collision by scoping revenue cache key with tenant_id --- backend/app/api/v1/dashboard.py | 5 ++++- backend/app/services/cache.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/app/api/v1/dashboard.py b/backend/app/api/v1/dashboard.py index 1ec352d7e3..cb178ee2a0 100644 --- a/backend/app/api/v1/dashboard.py +++ b/backend/app/api/v1/dashboard.py @@ -11,7 +11,10 @@ async def get_dashboard_summary( current_user: dict = Depends(get_current_user) ) -> Dict[str, Any]: - tenant_id = getattr(current_user, "tenant_id", "default_tenant") or "default_tenant" + if isinstance(current_user, dict): + tenant_id = current_user.get("tenant_id", "default_tenant") + else: + tenant_id = getattr(current_user, "tenant_id", "default_tenant") or "default_tenant" revenue_data = await get_revenue_summary(property_id, tenant_id) 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) From d0bea88b148cde53e3c3815ff3af31a9b9ad9614 Mon Sep 17 00:00:00 2001 From: ramazanberkozbek <20217170057@ogr.oku.edu.tr> Date: Fri, 16 Oct 2026 15:33:19 +0300 Subject: [PATCH 03/10] Fix financial precision mismatch in revenue calculation --- backend/app/api/v1/dashboard.py | 2 +- backend/app/services/reservations.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/app/api/v1/dashboard.py b/backend/app/api/v1/dashboard.py index cb178ee2a0..8dfbf2cff3 100644 --- a/backend/app/api/v1/dashboard.py +++ b/backend/app/api/v1/dashboard.py @@ -18,7 +18,7 @@ async def get_dashboard_summary( revenue_data = await get_revenue_summary(property_id, tenant_id) - total_revenue_float = float(revenue_data['total']) + total_revenue_float = round(float(revenue_data['total']), 2) return { "property_id": revenue_data['property_id'], diff --git a/backend/app/services/reservations.py b/backend/app/services/reservations.py index bcd936d108..00d130de34 100644 --- a/backend/app/services/reservations.py +++ b/backend/app/services/reservations.py @@ -1,5 +1,5 @@ from datetime import datetime -from decimal import Decimal +from decimal import Decimal, ROUND_HALF_UP from typing import Dict, Any, List async def calculate_monthly_revenue(property_id: str, month: int, year: int, db_session=None) -> Decimal: @@ -61,7 +61,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, From 00d1be515af1f14cf7443129246d94409aed9665 Mon Sep 17 00:00:00 2001 From: ramazanberkozbek <20217170057@ogr.oku.edu.tr> Date: Fri, 16 Oct 2026 15:37:22 +0300 Subject: [PATCH 04/10] Fix rare edge case where timezone offset shifts check-in to next month --- backend/app/services/reservations.py | 68 ++++++++++++++++++---------- 1 file changed, 44 insertions(+), 24 deletions(-) diff --git a/backend/app/services/reservations.py b/backend/app/services/reservations.py index 00d130de34..59f7d306f4 100644 --- a/backend/app/services/reservations.py +++ b/backend/app/services/reservations.py @@ -2,34 +2,54 @@ from decimal import Decimal, ROUND_HALF_UP from typing import Dict, Any, List -async def calculate_monthly_revenue(property_id: str, month: int, year: int, db_session=None) -> Decimal: +async def calculate_monthly_revenue( + property_id: str, + tenant_id: str, + month: int, + year: int, + db_session=None +) -> Decimal: """ - Calculates revenue for a specific month. + Calculates revenue for a specific month, accounting for property timezone. """ - - start_date = datetime(year, month, 1) - if month < 12: - end_date = datetime(year, month + 1, 1) - else: - end_date = datetime(year + 1, 1, 1) + try: + from app.core.database_pool import db_pool + from sqlalchemy import text + + await db_pool.initialize() - print(f"DEBUG: Querying revenue for {property_id} from {start_date} to {end_date}") + async def _execute_query(session): + query = text(""" + SELECT COALESCE(SUM(r.total_amount), 0) as total + FROM reservations r + JOIN properties p ON r.property_id = p.id AND r.tenant_id = p.tenant_id + WHERE r.property_id = :property_id + AND r.tenant_id = :tenant_id + AND EXTRACT(MONTH FROM (r.check_in_date AT TIME ZONE p.timezone)) = :month + AND EXTRACT(YEAR FROM (r.check_in_date AT TIME ZONE p.timezone)) = :year + """) + result = await session.execute(query, { + "property_id": property_id, + "tenant_id": tenant_id, + "month": month, + "year": year + }) + row = result.fetchone() + if row and row.total is not None: + return Decimal(str(row.total)).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP) + return Decimal('0.00') - # 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 + if db_session: + return await _execute_query(db_session) + elif db_pool.session_factory: + async with db_pool.get_session() as session: + return await _execute_query(session) + else: + return Decimal('0.00') + + except Exception as e: + print(f"Error calculating monthly revenue for {property_id}: {e}") + return Decimal('0.00') async def calculate_total_revenue(property_id: str, tenant_id: str) -> Dict[str, Any]: """ From 293fd41737cd67f86bc01bb292a2f05fcc6eed38 Mon Sep 17 00:00:00 2001 From: ramazanberkozbek <20217170057@ogr.oku.edu.tr> Date: Fri, 16 Oct 2026 15:46:38 +0300 Subject: [PATCH 05/10] Fix async session handling and add greenlet dependency for DatabasePool --- backend/app/core/database_pool.py | 3 +-- backend/requirements.txt | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/app/core/database_pool.py b/backend/app/core/database_pool.py index b5bc6a1694..2bd5dce829 100644 --- a/backend/app/core/database_pool.py +++ b/backend/app/core/database_pool.py @@ -22,7 +22,6 @@ async def initialize(self): self.engine = create_async_engine( db_url, - poolclass=QueuePool, pool_size=20, # Number of connections to maintain max_overflow=30, # Additional connections when needed pool_pre_ping=True, # Validate connections @@ -48,7 +47,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/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] From 5b3c86ff66012af824f680729025437a7355c12a Mon Sep 17 00:00:00 2001 From: ramazanberkozbek <20217170057@ogr.oku.edu.tr> Date: Fri, 16 Oct 2026 15:49:22 +0300 Subject: [PATCH 06/10] Normalize database URL with make_url for asyncpg driver --- backend/app/core/database_pool.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/backend/app/core/database_pool.py b/backend/app/core/database_pool.py index 2bd5dce829..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 @@ -16,9 +16,7 @@ async def initialize(self): if self.engine and self.session_factory: return try: - db_url = settings.database_url - if db_url.startswith("postgresql://"): - db_url = db_url.replace("postgresql://", "postgresql+asyncpg://", 1) + db_url = make_url(settings.database_url).set(drivername="postgresql+asyncpg") self.engine = create_async_engine( db_url, From aa43557439a2d2fc684e0a118afa92cba4fd345d Mon Sep 17 00:00:00 2001 From: ramazanberkozbek <20217170057@ogr.oku.edu.tr> Date: Fri, 16 Oct 2026 15:50:45 +0300 Subject: [PATCH 07/10] Raise database errors directly instead of falling back to fake revenue data --- backend/app/services/reservations.py | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/backend/app/services/reservations.py b/backend/app/services/reservations.py index 59f7d306f4..205fc69987 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, ROUND_HALF_UP from typing import Dict, Any, List +logger = logging.getLogger(__name__) + async def calculate_monthly_revenue( property_id: str, tenant_id: str, @@ -102,24 +105,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 From c4154beb789f807388acd5ca995c391dc8ac9f39 Mon Sep 17 00:00:00 2001 From: ramazanberkozbek <20217170057@ogr.oku.edu.tr> Date: Fri, 16 Oct 2026 16:02:22 +0300 Subject: [PATCH 08/10] Remove timestamp parameter in RevenueSummary to allow client-side caching --- frontend/src/components/RevenueSummary.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/components/RevenueSummary.tsx b/frontend/src/components/RevenueSummary.tsx index dbb6d0629d..90fe6fe6aa 100644 --- a/frontend/src/components/RevenueSummary.tsx +++ b/frontend/src/components/RevenueSummary.tsx @@ -28,8 +28,7 @@ export const RevenueSummary: React.FC = ({ propertyId = 'pr // 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() + simulatedTenant: activeTenant }); setData(response); } catch (err) { From 9b42a7993919e786ff8e305cd54832682379260d Mon Sep 17 00:00:00 2001 From: ramazanberkozbek <20217170057@ogr.oku.edu.tr> Date: Fri, 16 Oct 2026 16:20:01 +0300 Subject: [PATCH 09/10] Clean up dashboard endpoint and revenue handling --- backend/app/api/v1/dashboard.py | 28 ++++++++++++---------- backend/app/services/reservations.py | 4 ++-- frontend/src/components/RevenueSummary.tsx | 15 ++++-------- frontend/src/lib/secureApi.ts | 18 ++++---------- 4 files changed, 27 insertions(+), 38 deletions(-) diff --git a/backend/app/api/v1/dashboard.py b/backend/app/api/v1/dashboard.py index 8dfbf2cff3..aa06992950 100644 --- a/backend/app/api/v1/dashboard.py +++ b/backend/app/api/v1/dashboard.py @@ -5,24 +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]: - - if isinstance(current_user, dict): - tenant_id = current_user.get("tenant_id", "default_tenant") - else: - 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 = round(float(revenue_data['total']), 2) - + 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/services/reservations.py b/backend/app/services/reservations.py index 205fc69987..e4bf9d4f2e 100644 --- a/backend/app/services/reservations.py +++ b/backend/app/services/reservations.py @@ -51,7 +51,7 @@ async def _execute_query(session): return Decimal('0.00') except Exception as e: - print(f"Error calculating monthly revenue for {property_id}: {e}") + logger.error(f"Error calculating monthly revenue for {property_id}: {e}") return Decimal('0.00') async def calculate_total_revenue(property_id: str, tenant_id: str) -> Dict[str, Any]: @@ -64,10 +64,10 @@ async def calculate_total_revenue(property_id: str, tenant_id: str) -> Dict[str, 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, diff --git a/frontend/src/components/RevenueSummary.tsx b/frontend/src/components/RevenueSummary.tsx index 90fe6fe6aa..a2fae071c5 100644 --- a/frontend/src/components/RevenueSummary.tsx +++ b/frontend/src/components/RevenueSummary.tsx @@ -10,26 +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 - }); + const response = await SecureAPI.getDashboardSummary(propertyId); setData(response); } catch (err) { setError('Failed to load revenue data'); @@ -40,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', From 2f5f330529d1a445a398bfee8b70fb4881cc5593 Mon Sep 17 00:00:00 2001 From: ramazanberkozbek <20217170057@ogr.oku.edu.tr> Date: Fri, 16 Oct 2026 16:45:40 +0300 Subject: [PATCH 10/10] Revert unused calculate_monthly_revenue to original placeholder --- backend/app/services/reservations.py | 69 ++++++++++------------------ 1 file changed, 25 insertions(+), 44 deletions(-) diff --git a/backend/app/services/reservations.py b/backend/app/services/reservations.py index e4bf9d4f2e..8be12c0852 100644 --- a/backend/app/services/reservations.py +++ b/backend/app/services/reservations.py @@ -5,54 +5,35 @@ logger = logging.getLogger(__name__) -async def calculate_monthly_revenue( - property_id: str, - tenant_id: str, - month: int, - year: int, - db_session=None -) -> Decimal: +async def calculate_monthly_revenue(property_id: str, month: int, year: int, db_session=None) -> Decimal: """ - Calculates revenue for a specific month, accounting for property timezone. + Calculates revenue for a specific month. """ - try: - from app.core.database_pool import db_pool - from sqlalchemy import text - - await db_pool.initialize() + + start_date = datetime(year, month, 1) + if month < 12: + end_date = datetime(year, month + 1, 1) + else: + end_date = datetime(year + 1, 1, 1) - async def _execute_query(session): - query = text(""" - SELECT COALESCE(SUM(r.total_amount), 0) as total - FROM reservations r - JOIN properties p ON r.property_id = p.id AND r.tenant_id = p.tenant_id - WHERE r.property_id = :property_id - AND r.tenant_id = :tenant_id - AND EXTRACT(MONTH FROM (r.check_in_date AT TIME ZONE p.timezone)) = :month - AND EXTRACT(YEAR FROM (r.check_in_date AT TIME ZONE p.timezone)) = :year - """) - result = await session.execute(query, { - "property_id": property_id, - "tenant_id": tenant_id, - "month": month, - "year": year - }) - row = result.fetchone() - if row and row.total is not None: - return Decimal(str(row.total)).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP) - return Decimal('0.00') + 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 - if db_session: - return await _execute_query(db_session) - elif db_pool.session_factory: - async with db_pool.get_session() as session: - return await _execute_query(session) - else: - return Decimal('0.00') - - except Exception as e: - logger.error(f"Error calculating monthly revenue for {property_id}: {e}") - return Decimal('0.00') async def calculate_total_revenue(property_id: str, tenant_id: str) -> Dict[str, Any]: """