diff --git a/README.md b/README.md new file mode 100644 index 000000000..ff2ca43eb --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +Starting assignment. diff --git a/backend/app/api/v1/dashboard.py b/backend/app/api/v1/dashboard.py index 1ec352d7e..52d624754 100644 --- a/backend/app/api/v1/dashboard.py +++ b/backend/app/api/v1/dashboard.py @@ -1,25 +1,43 @@ -from fastapi import APIRouter, Depends, HTTPException +from decimal import Decimal, ROUND_HALF_UP +from fastapi import APIRouter, Depends, HTTPException, status from typing import Dict, Any from app.services.cache import get_revenue_summary from app.core.auth import authenticate_request as get_current_user +from app.api.v1.properties import _TENANT_PROPERTIES router = APIRouter() + +def _tenant_owns_property(tenant_id: str, property_id: str) -> bool: + """Return True if the property belongs to this tenant (DB fallback uses seed data).""" + owned = {p['id'] for p in _TENANT_PROPERTIES.get(tenant_id, [])} + return property_id in owned + + @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" - + + # Prevent tenants from querying properties that belong to other tenants. + if not _tenant_owns_property(tenant_id, property_id): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Property does not belong to your account." + ) + revenue_data = await get_revenue_summary(property_id, tenant_id) - - total_revenue_float = float(revenue_data['total']) - + + # Keep as Decimal to avoid float binary-representation errors (e.g. 333.333*3 in float + # produces 999.999... instead of 1000.000). Round to 2 decimal places for display. + total_revenue = Decimal(revenue_data['total']).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP) + return { "property_id": revenue_data['property_id'], - "total_revenue": total_revenue_float, + "total_revenue": str(total_revenue), "currency": revenue_data['currency'], "reservations_count": revenue_data['count'] } diff --git a/backend/app/api/v1/properties.py b/backend/app/api/v1/properties.py new file mode 100644 index 000000000..a0c12aee2 --- /dev/null +++ b/backend/app/api/v1/properties.py @@ -0,0 +1,54 @@ +from fastapi import APIRouter, Depends +from typing import Dict, Any +from app.core.auth import authenticate_request as get_current_user + +router = APIRouter() + +# Seed data mirrors database/seed.sql so the fallback matches actual DB contents. +_TENANT_PROPERTIES = { + 'tenant-a': [ + {'id': 'prop-001', 'name': 'Beach House Alpha', 'timezone': 'Europe/Paris'}, + {'id': 'prop-002', 'name': 'City Apartment Downtown', 'timezone': 'Europe/Paris'}, + {'id': 'prop-003', 'name': 'Country Villa Estate', 'timezone': 'Europe/Paris'}, + ], + 'tenant-b': [ + {'id': 'prop-001', 'name': 'Mountain Lodge Beta', 'timezone': 'America/New_York'}, + {'id': 'prop-004', 'name': 'Lakeside Cottage', 'timezone': 'America/New_York'}, + {'id': 'prop-005', 'name': 'Urban Loft Modern', 'timezone': 'America/New_York'}, + ], +} + + +@router.get("/properties") +async def list_properties( + current_user: dict = Depends(get_current_user) +) -> Dict[str, Any]: + tenant_id = getattr(current_user, "tenant_id", None) or "default_tenant" + + try: + from sqlalchemy import text + from app.core.database_pool import DatabasePool + + db_pool = DatabasePool() + await db_pool.initialize() + + if db_pool.session_factory: + async with db_pool.get_session() as session: + query = text(""" + SELECT id, name, timezone + FROM properties + WHERE tenant_id = :tenant_id + ORDER BY name + """) + result = await session.execute(query, {"tenant_id": tenant_id}) + rows = result.fetchall() + properties = [ + {"id": row.id, "name": row.name, "timezone": row.timezone} + for row in rows + ] + return {"items": properties, "total": len(properties)} + except Exception as e: + print(f"DB error fetching properties for tenant {tenant_id}: {e}") + + properties = _TENANT_PROPERTIES.get(tenant_id, []) + return {"items": properties, "total": len(properties)} diff --git a/backend/app/main.py b/backend/app/main.py index 00734b2fa..0e92315ef 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -29,6 +29,7 @@ persistent_auth, dashboard, login, + properties, ) from .monitoring.middleware import PerformanceMonitoringMiddleware @@ -184,6 +185,9 @@ async def lifespan(app: FastAPI): # Dashboard app.include_router(dashboard.router, prefix="/api/v1", tags=["dashboard"]) +# Properties (tenant-scoped) +app.include_router(properties.router, prefix="/api/v1", tags=["properties"]) + # Bootstrap & Settings (for AppContext) app.include_router(company_settings.router, prefix="/api/v1", tags=["company-settings"]) app.include_router(bootstrap.router, prefix="/api/v1", tags=["bootstrap"]) 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..74de08ee1 100644 --- a/backend/app/services/reservations.py +++ b/backend/app/services/reservations.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timezone from decimal import Decimal from typing import Dict, Any, List @@ -7,11 +7,13 @@ async def calculate_monthly_revenue(property_id: str, month: int, year: int, db_ Calculates revenue for a specific month. """ - start_date = datetime(year, month, 1) + # Use timezone-aware UTC datetimes so comparisons against DB timestamps + # (which are stored in UTC) are correct regardless of property timezone. + start_date = datetime(year, month, 1, tzinfo=timezone.utc) if month < 12: - end_date = datetime(year, month + 1, 1) + end_date = datetime(year, month + 1, 1, tzinfo=timezone.utc) else: - end_date = datetime(year + 1, 1, 1) + end_date = datetime(year + 1, 1, 1, tzinfo=timezone.utc) print(f"DEBUG: Querying revenue for {property_id} from {start_date} to {end_date}") diff --git a/frontend/src/components/Dashboard.tsx b/frontend/src/components/Dashboard.tsx index a21bba404..ff7702f5c 100644 --- a/frontend/src/components/Dashboard.tsx +++ b/frontend/src/components/Dashboard.tsx @@ -1,16 +1,27 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { RevenueSummary } from "./RevenueSummary"; +import { SecureAPI } from "../lib/secureApi"; -const PROPERTIES = [ - { id: 'prop-001', name: 'Beach House Alpha' }, - { id: 'prop-002', name: 'City Apartment Downtown' }, - { id: 'prop-003', name: 'Country Villa Estate' }, - { id: 'prop-004', name: 'Lakeside Cottage' }, - { id: 'prop-005', name: 'Urban Loft Modern' } -]; +interface Property { + id: string; + name: string; +} const Dashboard: React.FC = () => { - const [selectedProperty, setSelectedProperty] = useState('prop-001'); + const [properties, setProperties] = useState([]); + const [selectedProperty, setSelectedProperty] = useState(''); + + useEffect(() => { + SecureAPI.getProperties() + .then((result) => { + const props: Property[] = result.data || []; + setProperties(props); + if (props.length > 0) { + setSelectedProperty(props[0].id); + } + }) + .catch(console.error); + }, []); return (
@@ -35,7 +46,7 @@ const Dashboard: React.FC = () => { onChange={(e) => setSelectedProperty(e.target.value)} className="block w-full sm:w-auto min-w-[200px] px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 text-sm" > - {PROPERTIES.map((property) => ( + {properties.map((property) => ( @@ -46,7 +57,7 @@ const Dashboard: React.FC = () => {
- + {selectedProperty && }
diff --git a/frontend/src/components/RevenueSummary.tsx b/frontend/src/components/RevenueSummary.tsx index dbb6d0629..95d719062 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; } @@ -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 = parseFloat(data.total_revenue); 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(parseFloat(data.total_revenue) - displayTotal) > 0.000001 && showRaw && (