From 9e068e78ac4200303c79b38dabaab6f75aa0390d Mon Sep 17 00:00:00 2001 From: Hachem Ouanes Date: Tue, 13 Oct 2026 17:34:57 +0100 Subject: [PATCH] fix: cache isolation, timezone dates, currency precision --- backend/app/api/v1/dashboard.py | 21 ++++++++++++++------- backend/app/core/database_pool.py | 10 +++++----- backend/app/services/cache.py | 2 +- backend/app/services/reservations.py | 10 ++++++---- backend/requirements.txt | 2 +- frontend/src/components/RevenueSummary.tsx | 7 ++++--- 6 files changed, 31 insertions(+), 21 deletions(-) diff --git a/backend/app/api/v1/dashboard.py b/backend/app/api/v1/dashboard.py index 1ec352d7e..b4ed4d15d 100644 --- a/backend/app/api/v1/dashboard.py +++ b/backend/app/api/v1/dashboard.py @@ -1,25 +1,32 @@ from fastapi import APIRouter, Depends, HTTPException from typing import Dict, Any +from decimal import Decimal from app.services.cache import get_revenue_summary from app.core.auth import authenticate_request as get_current_user router = APIRouter() +CURRENCY_EXPONENTS = {'USD': 2, 'EUR': 2, 'GBP': 2, 'JPY': 0, 'KWD': 3} + @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']) - + + currency = revenue_data['currency'] + currency_exponent = CURRENCY_EXPONENTS.get(currency, 2) + total_decimal = Decimal(str(revenue_data['total'])) + amount_minor_units = int(total_decimal * (10 ** currency_exponent)) + return { "property_id": revenue_data['property_id'], - "total_revenue": total_revenue_float, - "currency": revenue_data['currency'], + "amount_minor_units": amount_minor_units, + "currency": currency, + "currency_exponent": currency_exponent, "reservations_count": revenue_data['count'] } diff --git a/backend/app/core/database_pool.py b/backend/app/core/database_pool.py index d638dfcfe..0721659c1 100644 --- a/backend/app/core/database_pool.py +++ b/backend/app/core/database_pool.py @@ -1,6 +1,6 @@ import asyncio from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker -from sqlalchemy.pool import QueuePool +from sqlalchemy.pool import AsyncAdaptedQueuePool import logging from ..config import settings @@ -15,11 +15,11 @@ 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, + poolclass=AsyncAdaptedQueuePool, pool_size=20, # Number of connections to maintain max_overflow=30, # Additional connections when needed pool_pre_ping=True, # Validate connections @@ -45,7 +45,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..672e3f9d6 100644 --- a/backend/app/services/cache.py +++ b/backend/app/services/cache.py @@ -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) diff --git a/backend/app/services/reservations.py b/backend/app/services/reservations.py index 384bd00ab..ff29b4b50 100644 --- a/backend/app/services/reservations.py +++ b/backend/app/services/reservations.py @@ -1,17 +1,19 @@ from datetime import datetime from decimal import Decimal from typing import Dict, Any, List +import pytz -async def calculate_monthly_revenue(property_id: str, month: int, year: int, db_session=None) -> Decimal: +async def calculate_monthly_revenue(property_id: str, month: int, year: int, db_session=None, timezone: str = 'UTC') -> Decimal: """ Calculates revenue for a specific month. """ + tz = pytz.timezone(timezone) - start_date = datetime(year, month, 1) + start_date = tz.localize(datetime(year, month, 1)).astimezone(pytz.utc) if month < 12: - end_date = datetime(year, month + 1, 1) + end_date = tz.localize(datetime(year, month + 1, 1)).astimezone(pytz.utc) else: - end_date = datetime(year + 1, 1, 1) + end_date = tz.localize(datetime(year + 1, 1, 1)).astimezone(pytz.utc) print(f"DEBUG: Querying revenue for {property_id} from {start_date} to {end_date}") diff --git a/backend/requirements.txt b/backend/requirements.txt index 6b777d2aa..06cb8bd4a 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -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] diff --git a/frontend/src/components/RevenueSummary.tsx b/frontend/src/components/RevenueSummary.tsx index dbb6d0629..034fa86c1 100644 --- a/frontend/src/components/RevenueSummary.tsx +++ b/frontend/src/components/RevenueSummary.tsx @@ -3,7 +3,8 @@ import { SecureAPI } from '../lib/secureApi'; interface RevenueData { property_id: string; - total_revenue: number; + amount_minor_units: number; + currency_exponent: number; currency: string; reservations_count: number; } @@ -61,7 +62,7 @@ export const RevenueSummary: React.FC = ({ propertyId = 'pr if (error) return
{error}
; if (!data) return null; - const displayTotal = Math.round(data.total_revenue * 100) / 100; + const displayTotal = data.amount_minor_units / Math.pow(10, data.currency_exponent); return (
@@ -104,7 +105,7 @@ export const RevenueSummary: React.FC = ({ propertyId = 'pr {/* Precision Warning Area */}
- {Math.abs(data.total_revenue - displayTotal) > 0.000001 && showRaw && ( + {false && showRaw && (