diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..17167547b --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +node_modules/ + +dist/ +build/ + +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +.DS_Store +.idea/ +*.swp + +__pycache__/ +*.py[cod] +.venv/ +venv/ +.pytest_cache/ + +*.timestamp-*.mjs +ackages/react-devtools-timeline/dist \ No newline at end of file diff --git a/NOTES.md b/NOTES.md new file mode 100644 index 000000000..37b61cfd9 --- /dev/null +++ b/NOTES.md @@ -0,0 +1,7 @@ +# Initial thoughts +- First red flag: Properties with same id (not actually a PK) but different tenant. Tenant check forgotten somewhere? Correct assumption. Cache key forgot tenant_id, shared cache hits across property_ids, cross tenant. Slight correction: Missed `PRIMARY KEY (id, tenant_id)` before, so DB schema *is* fine. +- Second red flag: Off by a few cents -> amounts stored in DB with sub-cent precision - good. Possibly incorrectly handled by backend or frontend? +- Wrong data for march: Frontend shows $1000 which matches revenue from batched insert statement. But separate insert into for tenant a is not taken into account. ID references timezone which could be the culprit, especially since the check in was during a leap year on feb. 29th right before midnight. Need to investigate! Bug was indeed SQL query not being tz aware. Updated the query and validated in docker shell using psql directly! + +# Docker issues(?) +- Trying to see if the caching issue is resolved, I stumbled across the following: The data I am seeing in the dashboard is not coming from the DB and just happened to match the seed. It was also identical for both tenants which I had thought was due to the caching issue, but now I realize, upon closer inspection of the docker logs, that the connection to the DB actually fails currently, and the application is returning dummy data (see `reservations.py`s Except block). Need to fix the connection to validate cache fix worked and also cent precision. Assuming revenue for tenant a will **not** be $1000 on the dot.. \ No newline at end of file diff --git a/backend/app/api/v2/dashboard.py b/backend/app/api/v2/dashboard.py new file mode 100644 index 000000000..b1ab80230 --- /dev/null +++ b/backend/app/api/v2/dashboard.py @@ -0,0 +1,25 @@ +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 + +router = APIRouter() + +@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 = revenue_data['total'] + + return { + "property_id": revenue_data['property_id'], + "total_revenue": total_revenue_float, + "currency": revenue_data['currency'], + "reservations_count": revenue_data['count'] + } 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..c222a8952 100644 --- a/backend/app/services/reservations.py +++ b/backend/app/services/reservations.py @@ -19,10 +19,13 @@ async def calculate_monthly_revenue(property_id: str, month: int, year: int, 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 + JOIN properties p + ON p.id = reservations.property_id + AND p.tenant_id = reservations.tenant_id + WHERE reservations.property_id = $1 + AND reservations.tenant_id = $2 + AND (check_in_date AT TIME ZONE p.timezone) >= $3 + AND (check_in_date AT TIME ZONE p.timezone) < $4 """ # In production this query executes against a database session. diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 000000000..17167547b --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,22 @@ +node_modules/ + +dist/ +build/ + +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +.DS_Store +.idea/ +*.swp + +__pycache__/ +*.py[cod] +.venv/ +venv/ +.pytest_cache/ + +*.timestamp-*.mjs +ackages/react-devtools-timeline/dist \ No newline at end of file diff --git a/frontend/src/components/RevenueSummary.tsx b/frontend/src/components/RevenueSummary.tsx index dbb6d0629..4846ec6ea 100644 --- a/frontend/src/components/RevenueSummary.tsx +++ b/frontend/src/components/RevenueSummary.tsx @@ -3,7 +3,7 @@ import { SecureAPI } from '../lib/secureApi'; interface RevenueData { property_id: string; - total_revenue: number; + total_revenue: string; currency: string; reservations_count: number; } @@ -27,7 +27,7 @@ export const RevenueSummary: React.FC = ({ propertyId = 'pr try { // Use SecureAPI to handle authentication automatically // We pass the simulatedTenant option which SecureAPI will attach as a header - const response = await SecureAPI.getDashboardSummary(propertyId, { + const response = await SecureAPI.getDashboardSummaryV2(propertyId, { simulatedTenant: activeTenant, timestamp: Date.now() }); @@ -61,7 +61,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 = Math.round(+data.total_revenue * 100) / 100; return (
@@ -104,7 +104,7 @@ export const RevenueSummary: React.FC = ({ propertyId = 'pr {/* Precision Warning Area */}
- {Math.abs(data.total_revenue - displayTotal) > 0.000001 && showRaw && ( + {Math.abs(+data.total_revenue - displayTotal) > 0.000001 && showRaw && (
diff --git a/frontend/src/lib/secureApi.ts b/frontend/src/lib/secureApi.ts index f85f04c90..ef73c2345 100644 --- a/frontend/src/lib/secureApi.ts +++ b/frontend/src/lib/secureApi.ts @@ -1468,6 +1468,26 @@ export class SecureAPIClient { return this.request(`/api/v1/dashboard/summary?${queryParams}`, requestOptions); } + // ============= DASHBOARD API ============= + /** + * Get dashboard summary with optional simulation header (V2!) + */ + async getDashboardSummaryV2(propertyId: string, options?: { simulatedTenant?: string, timestamp?: number }) { + const queryParams = new URLSearchParams({ property_id: propertyId }); + if (options?.timestamp) { + queryParams.append('_t', options.timestamp.toString()); + } + + const requestOptions: RequestInit = {}; + if (options?.simulatedTenant) { + requestOptions.headers = { + 'X-Simulated-Tenant': options.simulatedTenant + }; + } + + return this.request(`/api/v2/dashboard/summary?${queryParams}`, requestOptions); + } + async uploadCompanyLogo(logo_url: string) { return this.request('/api/v1/company-settings/logo', { method: 'POST',