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
96 changes: 96 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# =========================
# Environment / Secrets
# =========================
.env
.env.*
!.env.example

# =========================
# Python
# =========================
__pycache__/
*.py[cod]
*.pyo
*.pyd

.venv/
venv/
env/
ENV/

*.egg-info/
.eggs/
.pytest_cache/
.coverage
.coverage.*
htmlcov/
.mypy_cache/
.ruff_cache/

# =========================
# Node / Frontend
# =========================
frontend/node_modules/
node_modules/
frontend/dist/
node_modules/
dist/
build/
npm-debug.log*
yarn-debug.log*
yarn-error.log*

dist/
build/
.next/
.nuxt/
.cache/
.parcel-cache/
.vite/
*.tsbuildinfo
*.pem
*.key
*.crt

# =========================
# Logs
# =========================
*.log
logs/

# =========================
# Databases / Local Data
# =========================
*.sqlite
*.sqlite3
*.db
*.db-journal

# =========================
# Docker
# =========================
docker-data/
docker-volumes/
volumes/

# =========================
# IDEs
# =========================
.idea/
.vscode/
*.iml

# =========================
# OS
# =========================
.DS_Store
Thumbs.db
desktop.ini

# =========================
# Temporary files
# =========================
*.tmp
*.temp
*.swp
*~
97 changes: 78 additions & 19 deletions backend/app/api/v1/dashboard.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,84 @@
from fastapi import APIRouter, Depends, HTTPException
from typing import Dict, Any
from app.services.cache import get_revenue_summary
from app.core.auth import authenticate_request as get_current_user
from fastapi import APIRouter, Depends, Query
from sqlalchemy import text

from app.core.auth import authenticate_request
from app.core.database_pool import DatabasePool
from app.services.reservations import calculate_monthly_revenue

router = APIRouter()


@router.get("/dashboard/properties")
async def get_dashboard_properties(
current_user=Depends(authenticate_request),
):
tenant_id = current_user.tenant_id

if not tenant_id:
raise ValueError("Authenticated user has no tenant")

db_pool = DatabasePool()
await db_pool.initialize()

if not db_pool.session_factory:
raise RuntimeError("Database pool is not available")

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},
)

items = [
{"id": row.id, "name": row.name}
for row in result.fetchall()
]

return {"items": items, "total": len(items)}
finally:
await db_pool.close()


@router.get("/dashboard/summary")
async def get_dashboard_summary(
property_id: str,
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'])

return {
"property_id": revenue_data['property_id'],
"total_revenue": total_revenue_float,
"currency": revenue_data['currency'],
"reservations_count": revenue_data['count']
}
month: int = Query(..., ge=1, le=12),
year: int = Query(..., ge=2000, le=2100),
current_user=Depends(authenticate_request),
):
tenant_id = current_user.tenant_id

if not tenant_id:
raise ValueError("Authenticated user has no tenant")

db_pool = DatabasePool()
await db_pool.initialize()

if not db_pool.session_factory:
raise RuntimeError("Database pool is not available")

try:
async with db_pool.get_session() as session:
total = await calculate_monthly_revenue(
property_id=property_id,
tenant_id=tenant_id,
month=month,
year=year,
db_session=session,
)

return {
"property_id": property_id,
"tenant_id": tenant_id,
"total_revenue": str(total),
"currency": "USD",
}

finally:
await db_pool.close()
104 changes: 57 additions & 47 deletions backend/app/core/database_pool.py
Original file line number Diff line number Diff line change
@@ -1,60 +1,70 @@
import asyncio
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.pool import QueuePool
import logging
from typing import AsyncIterator

from sqlalchemy.ext.asyncio import (
AsyncSession,
async_sessionmaker,
create_async_engine,
)

from ..config import settings

logger = logging.getLogger(__name__)

class DatabasePool:
def __init__(self):
self.engine = None
self.session_factory = None

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

async def initialize(self) -> None:
"""Initialize the asynchronous database connection pool."""
if self.session_factory is not None:
return

database_url = settings.database_url
if database_url.startswith("postgresql://"):
database_url = database_url.replace(
"postgresql://",
"postgresql+asyncpg://",
1,
)

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

async def close(self):
"""Close database connections"""
if self.engine:
await self.engine.dispose()

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

self.engine = create_async_engine(
database_url,
pool_size=20,
max_overflow=30,
pool_pre_ping=True,
pool_recycle=3600,
echo=False,
)

self.session_factory = async_sessionmaker(
bind=self.engine,
class_=AsyncSession,
expire_on_commit=False,
)

def get_session(self) -> AsyncSession:
"""Create a database session."""
if self.session_factory is None:
raise RuntimeError("Database pool is not initialized")

return self.session_factory()

# Global database pool instance
async def close(self) -> None:
"""Close the database connection pool."""
if self.engine is not None:
await self.engine.dispose()

self.engine = None
self.session_factory = None


db_pool = DatabasePool()

async def get_db_session() -> AsyncSession:
"""Dependency to get database session"""

async def get_db_session() -> AsyncIterator[AsyncSession]:
"""FastAPI dependency that provides a database session."""
if db_pool.session_factory is None:
await db_pool.initialize()

async with db_pool.get_session() as session:
yield session
yield session
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:{tenant_id}:{property_id}"

# Try to get from cache
cached = await redis_client.get(cache_key)
Expand Down
Loading