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
21 changes: 14 additions & 7 deletions backend/app/api/v1/dashboard.py
Original file line number Diff line number Diff line change
@@ -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))
Comment on lines +21 to +24

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: Converting total_decimal to minor units using int() truncates decimal values instead of rounding, causing precision loss for amounts with three decimal places.
Severity: MEDIUM

Suggested Fix

Use round() before converting to int, or use decimal quantization: int(round(total_decimal * (10 ** currency_exponent))).

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/api/v1/dashboard.py#L21-L24

Potential issue: When converting `total_decimal` to minor units using `int(total_decimal
* (10 ** currency_exponent))`, the value is truncated toward zero instead of being
rounded. Because amounts are stored with NUMERIC(10, 3) precision, fractional cents
(such as 999.999) truncate down to 99999 instead of rounding to 100000, displaying as
999.99 instead of 1000.00 and losing up to one minor currency unit.

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


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']
}
10 changes: 5 additions & 5 deletions backend/app/core/database_pool.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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,
Comment on lines +18 to +22

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: A new DatabasePool is instantiated on every cache miss without disposing of existing engines, leading to database connection accumulation.
Severity: HIGH

Suggested Fix

Use a shared singleton instance of DatabasePool managed during application lifespan events, or ensure db_pool.close() / engine.dispose() is called after operations complete.

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/core/database_pool.py#L18-L22

Potential issue: Each cache miss in `calculate_total_revenue()` instantiates and
initializes a new `DatabasePool()` instance. With the corrected database URL
configuration, pool initialization succeeds but the newly created async engines are
never closed or disposed. Over time, unclosed engines and pooled connections accumulate
on each cache miss, risking database connection limit exhaustion.

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

pool_size=20, # Number of connections to maintain
max_overflow=30, # Additional connections when needed
pool_pre_ping=True, # Validate connections
Expand All @@ -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")
Expand Down
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
10 changes: 6 additions & 4 deletions backend/app/services/reservations.py
Original file line number Diff line number Diff line change
@@ -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}")

Expand Down
2 changes: 1 addition & 1 deletion backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/components/RevenueSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -61,7 +62,7 @@ export const RevenueSummary: React.FC<RevenueSummaryProps> = ({ propertyId = 'pr
if (error) return <div className="p-4 text-red-500 bg-red-50 rounded-lg">{error}</div>;
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 (
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition-shadow duration-300">
Expand Down Expand Up @@ -104,7 +105,7 @@ export const RevenueSummary: React.FC<RevenueSummaryProps> = ({ propertyId = 'pr

{/* Precision Warning Area */}
<div className="mt-4 h-6">
{Math.abs(data.total_revenue - displayTotal) > 0.000001 && showRaw && (
{false && showRaw && (
<div className="flex items-center text-xs text-amber-600 bg-amber-50 px-2 py-1 rounded">
<svg className="h-4 w-4 mr-1.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
Expand Down