From 72765e33fcc6273a7bde582670a5705dd9d3a96f Mon Sep 17 00:00:00 2001 From: Abhijna Date: Fri, 31 Jul 2026 15:49:31 +0200 Subject: [PATCH] fix by Abhijna Kanthila --- .gitignore | 3 + backend/app/api/v1/dashboard.py | 17 +++- backend/app/core/database_pool.py | 6 +- backend/app/services/cache.py | 3 +- backend/app/services/reservations.py | 108 ++++++++++++++------- frontend/src/components/Dashboard.tsx | 45 +++++++-- frontend/src/components/RevenueSummary.tsx | 16 +-- frontend/src/contexts/AuthContext.new.tsx | 10 ++ 8 files changed, 141 insertions(+), 67 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..a5d3b950b --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +*.py[cod] +before_fix.md \ No newline at end of file diff --git a/backend/app/api/v1/dashboard.py b/backend/app/api/v1/dashboard.py index 1ec352d7e..d87d74b93 100644 --- a/backend/app/api/v1/dashboard.py +++ b/backend/app/api/v1/dashboard.py @@ -15,11 +15,18 @@ 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 = float(revenue_data['total']) + # return { + # "property_id": revenue_data['property_id'], + # "total_revenue": total_revenue_float, + # "currency": revenue_data['currency'], + # "reservations_count": revenue_data['count'] + # } return { - "property_id": revenue_data['property_id'], - "total_revenue": total_revenue_float, - "currency": revenue_data['currency'], - "reservations_count": revenue_data['count'] + "property_id": revenue_data["property_id"], + "tenant_id": revenue_data["tenant_id"], + "total_revenue": revenue_data["total"], + "currency": revenue_data["currency"], + "reservations_count": revenue_data["count"], } diff --git a/backend/app/core/database_pool.py b/backend/app/core/database_pool.py index d638dfcfe..eea590f43 100644 --- a/backend/app/core/database_pool.py +++ b/backend/app/core/database_pool.py @@ -1,6 +1,5 @@ import asyncio from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker -from sqlalchemy.pool import QueuePool import logging from ..config import settings @@ -15,11 +14,10 @@ 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}" + database_url = settings.database_url.replace("postgresql://", "postgresql+asyncpg://", 1) 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 @@ -45,7 +43,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 b81474957..35b3ebbdf 100644 --- a/backend/app/services/cache.py +++ b/backend/app/services/cache.py @@ -10,7 +10,8 @@ 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:{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..0081118fb 100644 --- a/backend/app/services/reservations.py +++ b/backend/app/services/reservations.py @@ -1,6 +1,11 @@ -from datetime import datetime -from decimal import Decimal -from typing import Dict, Any, List +from datetime import datetime, timezone +from decimal import Decimal, ROUND_HALF_UP +from typing import Dict, Any +from zoneinfo import ZoneInfo + + +def format_money(amount: Decimal) -> str: + return str(amount.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)) async def calculate_monthly_revenue(property_id: str, month: int, year: int, db_session=None) -> Decimal: """ @@ -33,11 +38,12 @@ async def calculate_monthly_revenue(property_id: str, month: int, year: int, db_ async def calculate_total_revenue(property_id: str, tenant_id: str) -> Dict[str, Any]: """ - Aggregates revenue from database. + Aggregates March 2024 revenue from database using the property's local timezone. """ try: # Import database pool from app.core.database_pool import DatabasePool + from sqlalchemy import text # Initialize pool if needed db_pool = DatabasePool() @@ -45,33 +51,74 @@ 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 + property_query = text(""" + SELECT timezone + FROM properties + WHERE id = :property_id AND tenant_id = :tenant_id + """) + + property_result = await session.execute(property_query, { + "property_id": property_id, + "tenant_id": tenant_id + }) + property_row = property_result.fetchone() + + if not property_row: + return { + "property_id": property_id, + "tenant_id": tenant_id, + "total": "0.00", + "currency": "USD", + "count": 0 + } + + property_timezone = ZoneInfo(property_row.timezone or "UTC") + start_local = datetime(2024, 3, 1, tzinfo=property_timezone) + end_local = datetime(2024, 4, 1, tzinfo=property_timezone) + start_utc = start_local.astimezone(timezone.utc) + end_utc = end_local.astimezone(timezone.utc) + # Previous query summed all reservations for the property regardless of + # reporting month or property timezone: + # + # 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 + # """) + query = text(""" SELECT - property_id, SUM(total_amount) as total_revenue, - COUNT(*) as reservation_count + COUNT(*) as reservation_count, + COALESCE(MAX(currency), 'USD') as currency FROM reservations - WHERE property_id = :property_id AND tenant_id = :tenant_id - GROUP BY property_id + WHERE property_id = :property_id + AND tenant_id = :tenant_id + AND check_in_date >= :start_date + AND check_in_date < :end_date """) result = await session.execute(query, { "property_id": property_id, - "tenant_id": tenant_id + "tenant_id": tenant_id, + "start_date": start_utc, + "end_date": end_utc }) row = result.fetchone() if row: - total_revenue = Decimal(str(row.total_revenue)) + total_revenue = Decimal(str(row.total_revenue or "0")) return { "property_id": property_id, "tenant_id": tenant_id, - "total": str(total_revenue), - "currency": "USD", - "count": row.reservation_count + "total": format_money(total_revenue), + "currency": row.currency or "USD", + "count": row.reservation_count or 0 } else: # No reservations found for this property @@ -88,22 +135,15 @@ async def calculate_total_revenue(property_id: str, tenant_id: str) -> Dict[str, 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'] - } + # Previous fallback returned property-only mock data, which could hide + # tenant isolation bugs because tenants with the same property id received + # the same fake revenue: + # + # 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} + # } + raise diff --git a/frontend/src/components/Dashboard.tsx b/frontend/src/components/Dashboard.tsx index a21bba404..69475b4dc 100644 --- a/frontend/src/components/Dashboard.tsx +++ b/frontend/src/components/Dashboard.tsx @@ -1,16 +1,41 @@ -import React, { useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; +import { useAuth } from "../contexts/AuthContext.new"; import { RevenueSummary } from "./RevenueSummary"; -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' } -]; +// 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' } +// ]; + +const PROPERTIES_BY_TENANT: Record> = { + 'tenant-a': [ + { id: 'prop-001', name: 'Beach House Alpha' }, + { id: 'prop-002', name: 'City Apartment Downtown' }, + { id: 'prop-003', name: 'Country Villa Estate' } + ], + 'tenant-b': [ + { id: 'prop-001', name: 'Mountain Lodge Beta' }, + { id: 'prop-004', name: 'Lakeside Cottage' }, + { id: 'prop-005', name: 'Urban Loft Modern' } + ] +}; const Dashboard: React.FC = () => { + const { user } = useAuth(); const [selectedProperty, setSelectedProperty] = useState('prop-001'); + const tenantId = user?.tenant_id; + const properties = useMemo(() => { + return tenantId ? PROPERTIES_BY_TENANT[tenantId] || [] : []; + }, [tenantId]); + + useEffect(() => { + if (properties.length > 0 && !properties.some((property) => property.id === selectedProperty)) { + setSelectedProperty(properties[0].id); + } + }, [properties, selectedProperty]); return (
@@ -35,7 +60,7 @@ const Dashboard: React.FC = () => { onChange={(e) => setSelectedProperty(e.target.value)} className="block w-full sm:w-auto 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) => ( @@ -46,7 +71,7 @@ const Dashboard: React.FC = () => {
- + {properties.length > 0 && }
diff --git a/frontend/src/components/RevenueSummary.tsx b/frontend/src/components/RevenueSummary.tsx index dbb6d0629..05d4c188d 100644 --- a/frontend/src/components/RevenueSummary.tsx +++ b/frontend/src/components/RevenueSummary.tsx @@ -3,7 +3,8 @@ import { SecureAPI } from '../lib/secureApi'; interface RevenueData { property_id: string; - total_revenue: number; + tenant_id?: string; + total_revenue: string; currency: string; reservations_count: number; } @@ -61,7 +62,7 @@ export const RevenueSummary: React.FC = ({ propertyId = 'pr if (error) return
{error}
; if (!data) return null; - const displayTotal = Math.round(data.total_revenue * 100) / 100; + const displayTotal = Number(data.total_revenue); return (
@@ -102,17 +103,6 @@ export const RevenueSummary: React.FC = ({ propertyId = 'pr
- {/* Precision Warning Area */} -
- {Math.abs(data.total_revenue - displayTotal) > 0.000001 && showRaw && ( -
- - - - Precision Mismatch Detected -
- )} -
); diff --git a/frontend/src/contexts/AuthContext.new.tsx b/frontend/src/contexts/AuthContext.new.tsx index d19e30f18..e623d22b6 100644 --- a/frontend/src/contexts/AuthContext.new.tsx +++ b/frontend/src/contexts/AuthContext.new.tsx @@ -68,6 +68,11 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children source = 'user_metadata'; } + if (!tenant_id && enhancedUser.tenant_id) { + tenant_id = enhancedUser.tenant_id; + source = 'user'; + } + // Add tenant_id as a direct property for backward compatibility enhancedUser.tenant_id = tenant_id; @@ -105,6 +110,11 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children source = 'user_metadata'; } + if (!tenant_id && enhancedUser.tenant_id) { + tenant_id = enhancedUser.tenant_id; + source = 'user'; + } + // Add tenant_id as a direct property for backward compatibility enhancedUser.tenant_id = tenant_id;