Skip to content
Closed
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
22 changes: 22 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions NOTES.md
Original file line number Diff line number Diff line change
@@ -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..
25 changes: 25 additions & 0 deletions backend/app/api/v2/dashboard.py
Original file line number Diff line number Diff line change
@@ -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']
}
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
11 changes: 7 additions & 4 deletions backend/app/services/reservations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
22 changes: 22 additions & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
@@ -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
8 changes: 4 additions & 4 deletions frontend/src/components/RevenueSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -27,7 +27,7 @@ export const RevenueSummary: React.FC<RevenueSummaryProps> = ({ 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()
});
Expand Down Expand Up @@ -61,7 +61,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 = Math.round(+data.total_revenue * 100) / 100;

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 +104,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 && (
{Math.abs(+data.total_revenue - displayTotal) > 0.000001 && 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
20 changes: 20 additions & 0 deletions frontend/src/lib/secureApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1468,6 +1468,26 @@ export class SecureAPIClient {
return this.request<any>(`/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<any>(`/api/v2/dashboard/summary?${queryParams}`, requestOptions);
}

async uploadCompanyLogo(logo_url: string) {
return this.request<any>('/api/v1/company-settings/logo', {
method: 'POST',
Expand Down