Skip to content

Fix revenue dashboard accuracy, tenant isolation and precision bugs - #242

Open
beto-dt wants to merge 10 commits into
Base360-AI:mainfrom
beto-dt:main
Open

beto-dt wants to merge 10 commits into
Base360-AI:mainfrom
beto-dt:main

Conversation

@beto-dt

@beto-dt beto-dt commented Nov 11, 2026

Copy link
Copy Markdown

Summary

Investigated the three issues reported on the revenue dashboard and found six root causes. Each fix is in its own atomic commit.

Report Root cause Commit
Sunset: "March totals don't match" DB pool never connected, so the dashboard silently returned hardcoded mock data 17ab196, 98841be, ca9dc42
Sunset: "March totals don't match" Monthly revenue bucketed check-ins in UTC instead of the property's timezone f51a47d
Ocean: "seeing another company's revenue" Revenue cache key was revenue:{property_id}, shared across tenants 882625b
Finance: "totals off by a few cents" Money converted to float and rounded client-side with Math.round 6bc0990
(found while testing) Property dropdown hardcoded with every tenant's properties 0faa807, 2e53fed
(found while testing) Frontend sent an X-Simulated-Tenant debug header 5ea3a7d

Changes

1. Database connectivity (database_pool.py, requirements.txt)

  • Build the async engine from DATABASE_URL (the supabase_db_* settings it read don't exist).
  • Drop QueuePool, which is invalid for async engines.
  • Make get_session sync so async with db_pool.get_session() receives a session, not a coroutine.
  • Make initialize() idempotent.
  • Install sqlalchemy[asyncio] so greenlet is present on ARM images.

2. Remove the mock fallback (reservations.py, dashboard.py)

  • Reuse the shared pool instead of creating an engine per request (connection leak).
  • Remove the tenant-agnostic mock_data fallback. DB failures now return 503 with the error logged, instead of fabricated financial figures.

3. Tenant-scoped cache key (cache.py)

  • revenue:{property_id} → revenue:{tenant_id}:{property_id}. prop-001 exists for both tenants, so whoever loaded it first had their data served to the other tenant for 5 minutes.

4. Money precision (reservations.py, dashboard.py, RevenueSummary.tsx)

  • Quantize totals to 2 decimals with Decimal + ROUND_HALF_UP in the backend.
  • Serialize money as a string instead of float. The frontend only formats it.

5. Timezone-aware monthly revenue (reservations.py)

  • calculate_monthly_revenue now takes tenant_id, joins properties, and buckets check-ins with check_in_date AT TIME ZONE p.timezone.
  • Replaces the placeholder that always returned 0.

6. Tenant-scoped property list (dashboard.py, Dashboard.tsx, secureApi.ts, RevenueSummary.tsx)

  • New GET /api/v1/dashboard/properties, with the tenant resolved from the auth token only. Returns 403 if the tenant can't be resolved.
  • The dashboard loads properties from this endpoint instead of a hardcoded list.
  • Removed the X-Simulated-Tenant header. The tenant must never come from the client.

Verification

Tenant Property Before After
Sunset prop-001 (all time) 1,000.00 (mock) 2,250.00 (4 bookings)
Ocean prop-001 Sunset's figures (cache leak) 0.00
Ocean prop-004 1,776.50 (mock) 1,776.50 (real)
Sunset prop-001, March 2024 (Paris) 0 (placeholder) / 1,000.00 in UTC 2,250.00
Sunset prop-001, Feb 2024 (Paris) — 0.00
  • Logged in as both clients: each one sees only its own 3 properties, and /dashboard/summary returns 200 for every property.
  • Redis holds separate keys per tenant (revenue:tenant-a:prop-001, revenue:tenant-b:prop-001).
  • Stopping the DB returns 503 and a "Failed to load revenue data" message instead of mock numbers.
  • tsc --noEmit passes for the changed frontend files.

Deploy notes

  • Rebuild the backend image (new sqlalchemy[asyncio] dependency).
  • Flush the revenue cache after deploy: old revenue:{property_id} keys may hold cross-tenant data.

Follow-ups (out of scope)

  • /dashboard/summary should return 404 for a property outside the caller's tenant instead of 0.00.
  • Remove the default_tenant fallback in /dashboard/summary so it fails closed.
  • RLS is enabled on properties/reservations but has no policies.
  • JWT signing secret is 22 bytes (below the recommended 32).
  • Add tests for cache key isolation and month/timezone boundaries.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant