Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added backend/app/__pycache__/config.cpython-311.pyc
Binary file not shown.
Binary file added backend/app/__pycache__/database.cpython-311.pyc
Binary file not shown.
Binary file added backend/app/__pycache__/main.cpython-311.pyc
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
5 changes: 4 additions & 1 deletion backend/app/api/v1/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ 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 Exception:
raise HTTPException(status_code=503, detail="Revenue data is temporarily unavailable")
Comment on lines +16 to +19

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Catching all exceptions in get_dashboard_summary and converting them to HTTPException(503) swallows errors without logging tracebacks for non-database failures like Redis outages.
Severity: MEDIUM

Suggested Fix

Add a logger to backend/app/api/v1/dashboard.py and log the exception with logger.exception(...) within the except Exception block before raising the HTTPException.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: backend/app/api/v1/dashboard.py#L16-L19

Potential issue: In `get_dashboard_summary`, a bare `except Exception` catches all
exceptions arising from `get_revenue_summary` and raises an `HTTPException(503)` without
logging the exception details or traceback. Any failures outside the database path (such
as Redis connection outages, JSON serialization errors, or internal programming errors)
are swallowed silently without logs, preventing operators from diagnosing the root cause
of service unavailability.

Did we get this right? 👍 / 👎 to inform future reviews.


total_revenue_float = float(revenue_data['total'])

Expand Down
Binary file not shown.
Binary file added backend/app/core/__pycache__/auth.cpython-311.pyc
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
19 changes: 13 additions & 6 deletions backend/app/core/database_pool.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand All @@ -45,14 +43,23 @@ async def close(self):
if self.engine:
await self.engine.dispose()

async def get_session(self) -> AsyncSession:
"""Get database session from pool"""
def get_session(self) -> AsyncSession:
"""Get database session from pool (use with `async with`)"""
if not self.session_factory:
raise Exception("Database pool not initialized")
return self.session_factory()

# Global database pool instance
# Global database pool instance - shared by all requests
db_pool = DatabasePool()
_init_lock = asyncio.Lock()

async def get_db_pool() -> DatabasePool:
"""Return the shared pool, initializing it once on first use"""
if not db_pool.session_factory:
async with _init_lock:
if not db_pool.session_factory:
await db_pool.initialize()
return db_pool

async def get_db_session() -> AsyncSession:
"""Dependency to get database session"""
Expand Down
5 changes: 5 additions & 0 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,11 @@ async def lifespan(app: FastAPI):
await async_processor.shutdown()
logger.info("Async processor shutdown completed")

# Close the shared database pool
from .core.database_pool import db_pool
await db_pool.close()
logger.info("Database pool closed")

# Close connection pool
try:
from .core.supabase_connection_pool import supabase_pool
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
2 changes: 1 addition & 1 deletion backend/app/services/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:{property_id}:{tenant_id}"

# Try to get from cache
cached = await redis_client.get(cache_key)
Expand Down
35 changes: 10 additions & 25 deletions backend/app/services/reservations.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -36,12 +36,11 @@ 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
# Reuse the shared pool instead of creating a new engine per request
from app.core.database_pool import get_db_pool

# Initialize pool if needed
db_pool = DatabasePool()
await db_pool.initialize()
db_pool = await get_db_pool()

if db_pool.session_factory:
async with db_pool.get_session() as session:
Expand All @@ -65,7 +64,9 @@ 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))
# Amounts are stored with sub-cent precision; round the aggregated total once,
# in Decimal, so the API returns exact cents (float rounding drifts on x.xx5)
total_revenue = Decimal(str(row.total_revenue)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
return {
"property_id": property_id,
"tenant_id": tenant_id,
Expand All @@ -88,22 +89,6 @@ 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']
}
# Surface the failure instead of returning mock figures: fake numbers look real
# to clients, get cached, and the old mock data ignored tenant_id entirely
raise
2 changes: 1 addition & 1 deletion backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 2 additions & 0 deletions frontend/node_modules/vite/bin/vite.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.