diff --git a/backend/app/api/v1/dashboard.py b/backend/app/api/v1/dashboard.py index 1ec352d7e..50010045d 100644 --- a/backend/app/api/v1/dashboard.py +++ b/backend/app/api/v1/dashboard.py @@ -1,5 +1,6 @@ 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 @@ -8,15 +9,17 @@ @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']) + 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'], "total_revenue": total_revenue_float, diff --git a/backend/app/services/cache.py b/backend/app/services/cache.py index b81474957..2ca1c7eb4 100644 --- a/backend/app/services/cache.py +++ b/backend/app/services/cache.py @@ -1,16 +1,21 @@ +from fastapi import HTTPException import json import redis.asyncio as redis -from typing import Dict, Any +from typing import Dict, Any, Optional import os # 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: Optional[int] = None, year: Optional[int] = None) -> Dict[str, Any]: """ Fetches revenue summary, utilizing caching to improve performance. """ - cache_key = f"revenue:{property_id}" + + if (month is None) != (year is None): + raise HTTPException(status_code=400, detail="month and year are required fields") + + cache_key = f"revenue:{property_id}:tenant:{tenant_id}:month:{month}:year:{year}" # Try to get from cache cached = await redis_client.get(cache_key) @@ -21,7 +26,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, month, year) # 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..31b5aaaae 100644 --- a/backend/app/services/reservations.py +++ b/backend/app/services/reservations.py @@ -1,37 +1,68 @@ -from datetime import datetime +from datetime import datetime, timezone from decimal import Decimal -from typing import Dict, Any, List +from typing import Dict, Any, List, Optional +from zoneinfo import ZoneInfo -async def calculate_monthly_revenue(property_id: str, month: int, year: int, db_session=None) -> Decimal: +async def calculate_monthly_revenue(property_id: str, tenant_id:str, month: int, year: int, db_session=None) -> Dict[str, Any]: """ Calculates revenue for a specific month. """ + from app.core.database_pool import DatabasePool + from sqlalchemy import text - 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}") + db_pool = DatabasePool() + await db_pool.initialize() + if not db_pool.session_factory: + return { + "property_id": property_id, + "tenant_id": tenant_id, + "total": "0.00", + "currency": "USD", + "count": 0 + } - # 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 with db_pool.get_session() as session: + tz_result = await session.execute( + text("SELECT timezone FROM properties WHERE id = :property_id AND tenant_id = :tenant_id"), + {"property_id": property_id, "tenant_id": tenant_id} + ) + tz_row = tz_result.fetchone() + property_tz = ZoneInfo(tz_row.timezone if tz_row else "UTC") + + local_start = datetime(year, month, 1, tzinfo=property_tz) + if month < 12: + local_end = datetime(year, month + 1, 1, tzinfo=property_tz) + else: + local_end = datetime(year + 1, 1, 1, tzinfo=property_tz) + + start_utc = local_start.astimezone(timezone.utc) + end_utc = local_end.astimezone(timezone.utc) + + query = text(""" + SELECT SUM(total_amount) as total, COUNT(*) as cnt + FROM reservations + WHERE property_id = :property_id AND tenant_id = :tenant_id + AND check_in_date >= :start_date AND check_in_date < :end_date + """) + result = await session.execute(query, { + "property_id": property_id, + "tenant_id": tenant_id, + "start_date": start_utc, + "end_date": end_utc + }) + row = result.fetchone() + total = Decimal(str(row.total)) if row and row.total is not None else Decimal("0") + count = row.cnt if row and row.cnt is not None else 0 + + return { + "property_id": property_id, + "tenant_id": tenant_id, + "total": str(total), + "currency": "USD", + "count": count + } -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, month: Optional[int] = None, year: Optional[int] = None) -> Dict[str, Any]: """ Aggregates revenue from database. """ @@ -54,13 +85,15 @@ async def calculate_total_revenue(property_id: str, tenant_id: str) -> Dict[str, SUM(total_amount) as total_revenue, COUNT(*) as reservation_count FROM reservations - WHERE property_id = :property_id AND tenant_id = :tenant_id + WHERE property_id = :property_id AND tenant_id = :tenant_id AND date_part('month', check_in_date) = :month AND date_part('year', check_in_date) = :year GROUP BY property_id """) result = await session.execute(query, { "property_id": property_id, - "tenant_id": tenant_id + "tenant_id": tenant_id, + "month": month, + "year": year }) row = result.fetchone() @@ -91,14 +124,15 @@ async def calculate_total_revenue(property_id: str, tenant_id: str) -> Dict[str, # 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} + ('prop-001', 'tenant-a'): {'total': '1000.00', 'count': 3}, + ('prop-002', 'tenant-a'): {'total': '4975.50', 'count': 4}, + ('prop-003', 'tenant-a'): {'total': '6100.50', 'count': 2}, + ('prop-001', 'tenant-b'): {'total': '2340.75', 'count': 2}, + ('prop-004', 'tenant-b'): {'total': '1776.50', 'count': 4}, + ('prop-005', 'tenant-b'): {'total': '3256.00', 'count': 3} } - mock_property_data = mock_data.get(property_id, {'total': '0.00', 'count': 0}) + mock_property_data = mock_data.get((property_id, tenant_id), {'total': '0.00', 'count': 0}) return { "property_id": property_id,