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
57 changes: 51 additions & 6 deletions backend/app/api/v1/dashboard.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,70 @@
from fastapi import APIRouter, Depends, HTTPException
from typing import Dict, Any
from fastapi import APIRouter, Depends, HTTPException, Query
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,
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"

revenue_data = await get_revenue_summary(property_id, tenant_id)

total_revenue_float = float(revenue_data['total'])
try:
revenue_data = await get_revenue_summary(
property_id,
tenant_id,
year,
month,
)
except PropertyNotFoundError as error:
raise HTTPException(
status_code=404,
detail="Property not found",
) from error
except RevenueServiceError as error:
raise HTTPException(
status_code=503,
detail="Revenue data is temporarily unavailable",
) from error

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']
}
73 changes: 39 additions & 34 deletions backend/app/core/database_pool.py
Original file line number Diff line number Diff line change
@@ -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__)
Expand All @@ -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
6 changes: 6 additions & 0 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -90,6 +91,9 @@ async def lifespan(app: FastAPI):
# Startup
logger.info("Starting up...")

# Initialize the shared PostgreSQL connection pool.
await db_pool.initialize()
Comment on lines +94 to +95

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: The database pool initialization db_pool.initialize() lacks error handling. A misconfigured database URL will raise an unhandled exception and crash the application on startup.
Severity: HIGH

Suggested Fix

Wrap the await db_pool.initialize() call within a try/except block to catch potential exceptions. Log any errors that occur, similar to the error handling implemented for the Supabase and Redis initializations, to prevent the entire application from crashing during startup.

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/main.py#L94-L95

Potential issue: During application startup, the database connection pool initialization
via `db_pool.initialize()` is not wrapped in a `try/except` block. Functions called
within this initialization, such as `make_url()` or `create_async_engine()`, can raise
an `ArgumentError` or other exceptions if the database URL is malformed, a driver is
missing, or the database is unavailable. Since this call is not protected, any such
exception will be unhandled, causing the entire application to crash immediately upon
startup and preventing it from serving any requests. This is inconsistent with other
service initializations like Supabase and Redis, which are properly wrapped in error
handlers.

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


# Initialize Supabase connection pool
try:
from .core.supabase_connection_pool import supabase_pool
Expand Down Expand Up @@ -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
Expand Down
13 changes: 9 additions & 4 deletions backend/app/services/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,27 @@
# 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:{tenant_id}:{property_id}:{year}:{month}"

# Try to get from cache
cached = await redis_client.get(cache_key)
if cached:
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))
Expand Down
Loading