diff --git a/backend/app/__pycache__/config.cpython-311.pyc b/backend/app/__pycache__/config.cpython-311.pyc new file mode 100644 index 000000000..45053abf0 Binary files /dev/null and b/backend/app/__pycache__/config.cpython-311.pyc differ diff --git a/backend/app/__pycache__/database.cpython-311.pyc b/backend/app/__pycache__/database.cpython-311.pyc new file mode 100644 index 000000000..8522a33cd Binary files /dev/null and b/backend/app/__pycache__/database.cpython-311.pyc differ diff --git a/backend/app/__pycache__/main.cpython-311.pyc b/backend/app/__pycache__/main.cpython-311.pyc new file mode 100644 index 000000000..daed18d01 Binary files /dev/null and b/backend/app/__pycache__/main.cpython-311.pyc differ diff --git a/backend/app/api/v1/__pycache__/auth_info.cpython-311.pyc b/backend/app/api/v1/__pycache__/auth_info.cpython-311.pyc new file mode 100644 index 000000000..6a6431d66 Binary files /dev/null and b/backend/app/api/v1/__pycache__/auth_info.cpython-311.pyc differ diff --git a/backend/app/api/v1/__pycache__/bootstrap.cpython-311.pyc b/backend/app/api/v1/__pycache__/bootstrap.cpython-311.pyc new file mode 100644 index 000000000..5100b0f4c Binary files /dev/null and b/backend/app/api/v1/__pycache__/bootstrap.cpython-311.pyc differ diff --git a/backend/app/api/v1/__pycache__/cities.cpython-311.pyc b/backend/app/api/v1/__pycache__/cities.cpython-311.pyc new file mode 100644 index 000000000..0be2f116a Binary files /dev/null and b/backend/app/api/v1/__pycache__/cities.cpython-311.pyc differ diff --git a/backend/app/api/v1/__pycache__/city_access_fast.cpython-311.pyc b/backend/app/api/v1/__pycache__/city_access_fast.cpython-311.pyc new file mode 100644 index 000000000..36fb9169d Binary files /dev/null and b/backend/app/api/v1/__pycache__/city_access_fast.cpython-311.pyc differ diff --git a/backend/app/api/v1/__pycache__/city_access_fixed.cpython-311.pyc b/backend/app/api/v1/__pycache__/city_access_fixed.cpython-311.pyc new file mode 100644 index 000000000..02818a5d1 Binary files /dev/null and b/backend/app/api/v1/__pycache__/city_access_fixed.cpython-311.pyc differ diff --git a/backend/app/api/v1/__pycache__/company_settings.cpython-311.pyc b/backend/app/api/v1/__pycache__/company_settings.cpython-311.pyc new file mode 100644 index 000000000..f9292082d Binary files /dev/null and b/backend/app/api/v1/__pycache__/company_settings.cpython-311.pyc differ diff --git a/backend/app/api/v1/__pycache__/dashboard.cpython-311.pyc b/backend/app/api/v1/__pycache__/dashboard.cpython-311.pyc new file mode 100644 index 000000000..cdbe54e4f Binary files /dev/null and b/backend/app/api/v1/__pycache__/dashboard.cpython-311.pyc differ diff --git a/backend/app/api/v1/__pycache__/departments.cpython-311.pyc b/backend/app/api/v1/__pycache__/departments.cpython-311.pyc new file mode 100644 index 000000000..381986365 Binary files /dev/null and b/backend/app/api/v1/__pycache__/departments.cpython-311.pyc differ diff --git a/backend/app/api/v1/__pycache__/health.cpython-311.pyc b/backend/app/api/v1/__pycache__/health.cpython-311.pyc new file mode 100644 index 000000000..4a3860ee5 Binary files /dev/null and b/backend/app/api/v1/__pycache__/health.cpython-311.pyc differ diff --git a/backend/app/api/v1/__pycache__/login.cpython-311.pyc b/backend/app/api/v1/__pycache__/login.cpython-311.pyc new file mode 100644 index 000000000..01b858011 Binary files /dev/null and b/backend/app/api/v1/__pycache__/login.cpython-311.pyc differ diff --git a/backend/app/api/v1/__pycache__/persistent_auth.cpython-311.pyc b/backend/app/api/v1/__pycache__/persistent_auth.cpython-311.pyc new file mode 100644 index 000000000..e8801bad0 Binary files /dev/null and b/backend/app/api/v1/__pycache__/persistent_auth.cpython-311.pyc differ diff --git a/backend/app/api/v1/__pycache__/profile.cpython-311.pyc b/backend/app/api/v1/__pycache__/profile.cpython-311.pyc new file mode 100644 index 000000000..ed4f82808 Binary files /dev/null and b/backend/app/api/v1/__pycache__/profile.cpython-311.pyc differ diff --git a/backend/app/api/v1/__pycache__/users_lightning.cpython-311.pyc b/backend/app/api/v1/__pycache__/users_lightning.cpython-311.pyc new file mode 100644 index 000000000..d543a4bed Binary files /dev/null and b/backend/app/api/v1/__pycache__/users_lightning.cpython-311.pyc differ diff --git a/backend/app/api/v1/dashboard.py b/backend/app/api/v1/dashboard.py index 1ec352d7e..97e5b9a13 100644 --- a/backend/app/api/v1/dashboard.py +++ b/backend/app/api/v1/dashboard.py @@ -1,25 +1,97 @@ -from fastapi import APIRouter, Depends, HTTPException -from typing import Dict, Any +from fastapi import APIRouter, Depends, HTTPException, Query +from typing import Dict, Any, List from app.services.cache import get_revenue_summary from app.core.auth import authenticate_request as get_current_user +from app.models.auth import AuthenticatedUser +import logging +from app.config import settings +import asyncpg +from datetime import datetime, timezone router = APIRouter() +logger = logging.getLogger(__name__) @router.get("/dashboard/summary") async def get_dashboard_summary( property_id: str, + timestamp: int = Query(..., alias="_t"), current_user: dict = Depends(get_current_user) ) -> Dict[str, Any]: tenant_id = getattr(current_user, "tenant_id", "default_tenant") or "default_tenant" + requested_date = datetime.fromtimestamp( + timestamp / 1000, + tz=timezone.utc, + ) + year = requested_date.year + month = requested_date.month - revenue_data = await get_revenue_summary(property_id, tenant_id) + revenue_data = await get_revenue_summary(property_id, tenant_id, year, month) - 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, + # "total_revenue": total_revenue_float, + "total_revenue": revenue_data['total'], "currency": revenue_data['currency'], "reservations_count": revenue_data['count'] } + +@router.get("/dashboard/properties") +async def get_dashboard_properties( + current_user: AuthenticatedUser = Depends(get_current_user), +) -> List[Dict[str, Any]]: + """ + Return only properties belonging to the authenticated user's tenant. + """ + + tenant_id = current_user.tenant_id + + if not tenant_id: + raise HTTPException( + status_code=403, + detail="Authenticated user has no tenant", + ) + + connection = None + + try: + connection = await asyncpg.connect(settings.database_url) + + rows = await connection.fetch( + """ + SELECT + id, + name, + timezone + FROM properties + WHERE tenant_id = $1 + ORDER BY name + """, + tenant_id, + ) + + return [ + { + "id": row["id"], + "name": row["name"], + "timezone": row["timezone"], + } + for row in rows + ] + + except Exception: + logger.exception( + "Failed to load properties for tenant %s", + tenant_id, + ) + + raise HTTPException( + status_code=503, + detail="Properties are temporarily unavailable", + ) + + finally: + if connection is not None: + await connection.close() \ No newline at end of file diff --git a/backend/app/core/__pycache__/async_processing.cpython-311.pyc b/backend/app/core/__pycache__/async_processing.cpython-311.pyc new file mode 100644 index 000000000..f0b0d146e Binary files /dev/null and b/backend/app/core/__pycache__/async_processing.cpython-311.pyc differ diff --git a/backend/app/core/__pycache__/auth.cpython-311.pyc b/backend/app/core/__pycache__/auth.cpython-311.pyc new file mode 100644 index 000000000..ea87a732b Binary files /dev/null and b/backend/app/core/__pycache__/auth.cpython-311.pyc differ diff --git a/backend/app/core/__pycache__/circuit_breaker_fallback.cpython-311.pyc b/backend/app/core/__pycache__/circuit_breaker_fallback.cpython-311.pyc new file mode 100644 index 000000000..60909c949 Binary files /dev/null and b/backend/app/core/__pycache__/circuit_breaker_fallback.cpython-311.pyc differ diff --git a/backend/app/core/__pycache__/database_pool.cpython-311.pyc b/backend/app/core/__pycache__/database_pool.cpython-311.pyc new file mode 100644 index 000000000..2b1ce7d3a Binary files /dev/null and b/backend/app/core/__pycache__/database_pool.cpython-311.pyc differ diff --git a/backend/app/core/__pycache__/persistent_sessions.cpython-311.pyc b/backend/app/core/__pycache__/persistent_sessions.cpython-311.pyc new file mode 100644 index 000000000..cb2c1c12f Binary files /dev/null and b/backend/app/core/__pycache__/persistent_sessions.cpython-311.pyc differ diff --git a/backend/app/core/__pycache__/redis_client.cpython-311.pyc b/backend/app/core/__pycache__/redis_client.cpython-311.pyc new file mode 100644 index 000000000..84f8d6d76 Binary files /dev/null and b/backend/app/core/__pycache__/redis_client.cpython-311.pyc differ diff --git a/backend/app/core/__pycache__/supabase_connection_pool.cpython-311.pyc b/backend/app/core/__pycache__/supabase_connection_pool.cpython-311.pyc new file mode 100644 index 000000000..74d68da64 Binary files /dev/null and b/backend/app/core/__pycache__/supabase_connection_pool.cpython-311.pyc differ diff --git a/backend/app/core/__pycache__/tenant_cache.cpython-311.pyc b/backend/app/core/__pycache__/tenant_cache.cpython-311.pyc new file mode 100644 index 000000000..3c2c02d9a Binary files /dev/null and b/backend/app/core/__pycache__/tenant_cache.cpython-311.pyc differ diff --git a/backend/app/core/__pycache__/tenant_context.cpython-311.pyc b/backend/app/core/__pycache__/tenant_context.cpython-311.pyc new file mode 100644 index 000000000..389b8287d Binary files /dev/null and b/backend/app/core/__pycache__/tenant_context.cpython-311.pyc differ diff --git a/backend/app/core/__pycache__/tenant_resolver.cpython-311.pyc b/backend/app/core/__pycache__/tenant_resolver.cpython-311.pyc new file mode 100644 index 000000000..0c22ef714 Binary files /dev/null and b/backend/app/core/__pycache__/tenant_resolver.cpython-311.pyc differ diff --git a/backend/app/core/__pycache__/token_encryption.cpython-311.pyc b/backend/app/core/__pycache__/token_encryption.cpython-311.pyc new file mode 100644 index 000000000..f45abf6e7 Binary files /dev/null and b/backend/app/core/__pycache__/token_encryption.cpython-311.pyc differ diff --git a/backend/app/core/database_pool.py b/backend/app/core/database_pool.py index d638dfcfe..0cf08beb7 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.pool import QueuePool import logging from ..config import settings @@ -13,30 +13,43 @@ 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}" - + database_url = settings.database_url + + # SQLAlchemy async engine requires the asyncpg driver. + if database_url.startswith("postgresql://"): + database_url = 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 - pool_recycle=3600, # Recycle connections every hour - echo=False # Set to True for SQL debugging + pool_size=settings.database_pool_size, + max_overflow=settings.database_max_overflow, + pool_pre_ping=True, + pool_recycle=settings.database_pool_recycle, + echo=False, ) - + self.session_factory = async_sessionmaker( bind=self.engine, class_=AsyncSession, - expire_on_commit=False + expire_on_commit=False, ) - - logger.info("✅ Database connection pool initialized") - - except Exception as e: - logger.error(f"❌ Database pool initialization failed: {e}") + + logger.info("Database connection pool initialized") + + except Exception as error: + logger.exception( + "Database pool initialization failed: %s", + error, + ) + self.engine = None self.session_factory = None diff --git a/backend/app/models/__pycache__/auth.cpython-311.pyc b/backend/app/models/__pycache__/auth.cpython-311.pyc new file mode 100644 index 000000000..4664fb09f Binary files /dev/null and b/backend/app/models/__pycache__/auth.cpython-311.pyc differ diff --git a/backend/app/models/__pycache__/profile.cpython-311.pyc b/backend/app/models/__pycache__/profile.cpython-311.pyc new file mode 100644 index 000000000..4cadce1e4 Binary files /dev/null and b/backend/app/models/__pycache__/profile.cpython-311.pyc differ diff --git a/backend/app/monitoring/__pycache__/__init__.cpython-311.pyc b/backend/app/monitoring/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 000000000..3307ce681 Binary files /dev/null and b/backend/app/monitoring/__pycache__/__init__.cpython-311.pyc differ diff --git a/backend/app/monitoring/__pycache__/middleware.cpython-311.pyc b/backend/app/monitoring/__pycache__/middleware.cpython-311.pyc new file mode 100644 index 000000000..876795ee6 Binary files /dev/null and b/backend/app/monitoring/__pycache__/middleware.cpython-311.pyc differ diff --git a/backend/app/monitoring/__pycache__/performance.cpython-311.pyc b/backend/app/monitoring/__pycache__/performance.cpython-311.pyc new file mode 100644 index 000000000..dc9d1c1e1 Binary files /dev/null and b/backend/app/monitoring/__pycache__/performance.cpython-311.pyc differ diff --git a/backend/app/services/__pycache__/cache.cpython-311.pyc b/backend/app/services/__pycache__/cache.cpython-311.pyc new file mode 100644 index 000000000..a494b7fb8 Binary files /dev/null and b/backend/app/services/__pycache__/cache.cpython-311.pyc differ diff --git a/backend/app/services/__pycache__/properties.cpython-311.pyc b/backend/app/services/__pycache__/properties.cpython-311.pyc new file mode 100644 index 000000000..44a9c9a20 Binary files /dev/null and b/backend/app/services/__pycache__/properties.cpython-311.pyc differ diff --git a/backend/app/services/__pycache__/reservations.cpython-311.pyc b/backend/app/services/__pycache__/reservations.cpython-311.pyc new file mode 100644 index 000000000..b38332e2b Binary files /dev/null and b/backend/app/services/__pycache__/reservations.cpython-311.pyc differ diff --git a/backend/app/services/cache.py b/backend/app/services/cache.py index b81474957..7ef763ea9 100644 --- a/backend/app/services/cache.py +++ b/backend/app/services/cache.py @@ -6,11 +6,12 @@ # 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:{property_id}" + # cache_key = f"revenue:{property_id}" + cache_key = f"revenue:{property_id}:{tenant_id}:{year}:{month:02d}" # Try to get from cache cached = await redis_client.get(cache_key) @@ -21,7 +22,7 @@ async def get_revenue_summary(property_id: str, tenant_id: str) -> Dict[str, Any from app.services.reservations import calculate_total_revenue # Calculate revenue - result = await calculate_total_revenue(property_id, tenant_id) + result = await calculate_total_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 384bd00ab..f6c323db8 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 +import logging +from sqlalchemy import text +from app.core.database_pool import db_pool + +logger = logging.getLogger(__name__) async def calculate_monthly_revenue(property_id: str, month: int, year: int, db_session=None) -> Decimal: """ @@ -31,79 +36,76 @@ 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]: +async def calculate_total_revenue(property_id: str, tenant_id: str, year: int, month: int) -> Dict[str, Any]: + """ - Aggregates revenue from database. + Calculate monthly revenue for one tenant's property. + + March 2024 is the default reporting period for this assignment. """ - try: - # Import database pool - from app.core.database_pool import DatabasePool - - # Initialize pool if needed - db_pool = DatabasePool() + + # Initialize the shared database pool when needed. + if db_pool.session_factory is None: 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} - } - - mock_property_data = mock_data.get(property_id, {'total': '0.00', 'count': 0}) - + + if db_pool.session_factory is None: + raise RuntimeError("Database pool is unavailable") + + session = await db_pool.get_session() + + try: + result = await session.execute( + text( + """ + SELECT + COALESCE(SUM(r.total_amount), 0) AS total_revenue, + COUNT(*) AS reservation_count + FROM reservations AS r + JOIN properties AS p + ON p.id = r.property_id + AND p.tenant_id = r.tenant_id + WHERE r.property_id = :property_id + AND r.tenant_id = :tenant_id + + -- CHANGED: + -- Convert the reservation time into the property's + -- local timezone before checking the month and year. + AND EXTRACT( + YEAR FROM + r.check_in_date AT TIME ZONE p.timezone + ) = :year + + AND EXTRACT( + MONTH FROM + r.check_in_date AT TIME ZONE p.timezone + ) = :month + """ + ), + { + "property_id": property_id, + "tenant_id": tenant_id, + "year": year, + "month": month, + }, + ) + + row = result.one() + + total_revenue = Decimal(str(row.total_revenue)) + return { "property_id": property_id, - "tenant_id": tenant_id, - "total": mock_property_data['total'], + "tenant_id": tenant_id, + "total": str(total_revenue), "currency": "USD", - "count": mock_property_data['count'] + "count": row.reservation_count, } + + except Exception: + # CHANGED: + # Do not return hardcoded fake financial data. + # Let the API report the actual database failure. + raise + + finally: + await session.close() \ No newline at end of file diff --git a/frontend/src/components/Dashboard.tsx b/frontend/src/components/Dashboard.tsx index a21bba404..4f9490e3c 100644 --- a/frontend/src/components/Dashboard.tsx +++ b/frontend/src/components/Dashboard.tsx @@ -1,16 +1,61 @@ -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' } -]; +// 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; + timezone: string; +} const Dashboard: React.FC = () => { - const [selectedProperty, setSelectedProperty] = useState('prop-001'); + // const [selectedProperty, setSelectedProperty] = useState('prop-001'); + const [properties, setProperties] = useState([]); + const [selectedProperty, setSelectedProperty] = useState(""); + const [loadingProperties, setLoadingProperties] = useState(true); + + useEffect(() => { + const fetchProperties = async () => { + setLoadingProperties(true); + + try { + // The backend gets tenant_id from the logged-in user's token. + // The frontend does not send or choose a tenant ID. + const response = + await SecureAPI.getDashboardProperties(); + + setProperties(response); + + // Automatically select the first property belonging + // to the current tenant. + if (response.length > 0) { + setSelectedProperty(response[0].id); + } else { + setSelectedProperty(""); + } + } catch (error) { + console.error( + "Failed to load tenant properties:", + error + ); + + setProperties([]); + setSelectedProperty(""); + } finally { + setLoadingProperties(false); + } + }; + + fetchProperties(); + }, []); return (
@@ -33,12 +78,37 @@ const Dashboard: React.FC = () => {
@@ -46,7 +116,9 @@ const Dashboard: React.FC = () => {
- + {selectedProperty && ( + + )}
diff --git a/frontend/src/components/RevenueSummary.tsx b/frontend/src/components/RevenueSummary.tsx index dbb6d0629..ab113fb5c 100644 --- a/frontend/src/components/RevenueSummary.tsx +++ b/frontend/src/components/RevenueSummary.tsx @@ -29,7 +29,8 @@ export const RevenueSummary: React.FC = ({ propertyId = 'pr // We pass the simulatedTenant option which SecureAPI will attach as a header const response = await SecureAPI.getDashboardSummary(propertyId, { simulatedTenant: activeTenant, - timestamp: Date.now() + timestamp: 1710504000000 + // timestamp: Date.now() }); setData(response); } catch (err) { diff --git a/frontend/src/lib/secureApi.ts b/frontend/src/lib/secureApi.ts index f85f04c90..f842fa5db 100644 --- a/frontend/src/lib/secureApi.ts +++ b/frontend/src/lib/secureApi.ts @@ -68,6 +68,22 @@ export class SecureAPIClient { return SecureAPIClient.instance; } + async getDashboardProperties(): Promise< + Array<{ + id: string; + name: string; + timezone: string; + }> + > { + return this.request< + Array<{ + id: string; + name: string; + timezone: string; + }> + >("/api/v1/dashboard/properties"); + } + /** * Intercepts and blocks direct Supabase queries in development */