diff --git a/backend/app/api/v1/dashboard.py b/backend/app/api/v1/dashboard.py index 1ec352d7e..1c1a2753d 100644 --- a/backend/app/api/v1/dashboard.py +++ b/backend/app/api/v1/dashboard.py @@ -1,21 +1,32 @@ from fastapi import APIRouter, Depends, HTTPException -from typing import Dict, Any +from typing import Dict, Any, Optional +from decimal import Decimal, ROUND_HALF_UP from app.services.cache import get_revenue_summary from app.core.auth import authenticate_request as get_current_user router = APIRouter() +# Seed data only covers March 2024; default to that period +DEFAULT_MONTH = 3 +DEFAULT_YEAR = 2024 + @router.get("/dashboard/summary") async def get_dashboard_summary( property_id: str, + month: Optional[int] = None, + year: Optional[int] = None, 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']) + + month = month or DEFAULT_MONTH + year = year or DEFAULT_YEAR + + revenue_data = await get_revenue_summary(property_id, tenant_id, month, year) + + total_revenue_decimal = Decimal(revenue_data['total']).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) + total_revenue_float = float(total_revenue_decimal) return { "property_id": revenue_data['property_id'], diff --git a/backend/app/core/database_pool.py b/backend/app/core/database_pool.py index d638dfcfe..6bb7ddd10 100644 --- a/backend/app/core/database_pool.py +++ b/backend/app/core/database_pool.py @@ -1,6 +1,4 @@ -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 +13,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 +42,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..8428969dd 100644 --- a/backend/app/services/cache.py +++ b/backend/app/services/cache.py @@ -6,11 +6,11 @@ # 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, month: int, year: int) -> 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}:{year}-{month:02d}" # Try to get from cache cached = await redis_client.get(cache_key) @@ -18,12 +18,12 @@ async def get_revenue_summary(property_id: str, tenant_id: str) -> Dict[str, Any 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, month, year) # Cache the result for 5 minutes - await redis_client.setex(cache_key, 300, json.dumps(result)) + await redis_client.setex(cache_key, 30, json.dumps(result)) return result diff --git a/backend/app/services/reservations.py b/backend/app/services/reservations.py index 384bd00ab..2ae509737 100644 --- a/backend/app/services/reservations.py +++ b/backend/app/services/reservations.py @@ -1,109 +1,66 @@ -from datetime import datetime from decimal import Decimal -from typing import Dict, Any, List +from typing import Dict, Any -async def calculate_monthly_revenue(property_id: str, month: int, year: int, db_session=None) -> Decimal: - """ - Calculates revenue for a specific month. - """ +# Fallback data keyed by (tenant_id, property_id) so tenants never see each other's mock numbers. +# Values mirror the actual March 2024 totals in database/seed.sql +_MOCK_REVENUE = { + ("tenant-a", "prop-001"): {"total": "2250.00", "count": 4}, + ("tenant-a", "prop-002"): {"total": "4975.50", "count": 4}, + ("tenant-a", "prop-003"): {"total": "6100.50", "count": 2}, + ("tenant-b", "prop-001"): {"total": "0.00", "count": 0}, + ("tenant-b", "prop-004"): {"total": "1776.50", "count": 4}, + ("tenant-b", "prop-005"): {"total": "3256.00", "count": 3}, +} - start_date = datetime(year, month, 1) - if month < 12: - end_date = datetime(year, month + 1, 1) - else: - end_date = datetime(year + 1, 1, 1) - - print(f"DEBUG: Querying revenue for {property_id} from {start_date} to {end_date}") - - # SQL Simulation (This would be executed against the actual DB) - query = """ - SELECT SUM(total_amount) as total - FROM reservations - WHERE property_id = $1 - AND tenant_id = $2 - AND check_in_date >= $3 - AND check_in_date < $4 - """ - - # In production this query executes against a database session. - # result = await db.fetch_val(query, property_id, tenant_id, start_date, end_date) - # return result or Decimal('0') - - 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_monthly_revenue(property_id: str, tenant_id: str, month: int, year: int) -> Dict[str, Any]: """ - Aggregates revenue from database. + Calculates revenue for a specific month in the local timezone. + Falls back to tenant-scoped mock data if the database is unavailable. """ try: - # Import database pool from app.core.database_pool import DatabasePool - - # Initialize pool if needed + from sqlalchemy import text + db_pool = DatabasePool() 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: + + if not db_pool.session_factory: 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} + + async with db_pool.get_session() as session: + query = text(""" + SELECT SUM(r.total_amount) as total_revenue, COUNT(*) as reservation_count + FROM reservations r + JOIN properties 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 + AND date_trunc('month', r.check_in_date AT TIME ZONE p.timezone) = make_date(:year, :month, 1) + """) + row = (await session.execute(query, { + "property_id": property_id, + "tenant_id": tenant_id, + "year": year, + "month": month, + })).fetchone() + + total_revenue = Decimal(str(row.total_revenue)) if row and row.total_revenue is not None else Decimal("0.00") + count = row.reservation_count if row else 0 + + return { + "property_id": property_id, + "tenant_id": tenant_id, + "total": str(total_revenue), + "currency": "USD", + "count": count, } - - mock_property_data = mock_data.get(property_id, {'total': '0.00', 'count': 0}) - + + except Exception: + mock = _MOCK_REVENUE.get((tenant_id, property_id), {"total": "0.00", "count": 0}) return { "property_id": property_id, - "tenant_id": tenant_id, - "total": mock_property_data['total'], + "tenant_id": tenant_id, + "total": mock["total"], "currency": "USD", - "count": mock_property_data['count'] + "count": mock["count"], } diff --git a/package-lock.json b/package-lock.json index 30f49b2c1..4a151e222 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "The-Flex-PMS-Staging", + "name": "New_devs_App", "lockfileVersion": 3, "requires": true, "packages": {