Skip to content

Fix/revenue dashboard bugs - #220

Open
ansab-rehman wants to merge 6 commits into
Base360-AI:mainfrom
ansab-rehman:fix/revenue-dashboard-bugs
Open

ansab-rehman wants to merge 6 commits into
Base360-AI:mainfrom
ansab-rehman:fix/revenue-dashboard-bugs

Conversation

@ansab-rehman

Copy link
Copy Markdown

Production Bug Investigation & Resolution

Investigated three client reports: Sunset’s March revenue mismatch, Ocean seeing another company’s figures, and Finance reporting discrepancies of a few cents.

Identified six defects and fixed them across six commits. The broken connection pool and the mock-data fallback that concealed it were treated as separate issues and addressed independently.

Key finding: Every dashboard figure was fabricated before this branch. The database was never successfully queried because an exception handler swallowed the connection failure and returned a hardcoded table of plausible-looking numbers. This masked the underlying defects and made the reports difficult to diagnose.

No rebuild was required. All changes were surgical edits to existing functions, preserving the current structure and coding patterns.

Defects identified and fixed

1. Revenue cache key ignored the tenant

Files: cache.py

  • Symptom: Ocean intermittently saw Sunset’s revenue, creating a tenant privacy issue.

  • Root cause: The cache key was f"revenue:{property_id}", ignoring the existing tenant_id parameter. Property IDs were unique only within each tenant, so both tenants could use prop-001. Whichever tenant populated the cache first could serve its figures to the other for the entire 300-second TTL.

  • Fix: Updated the key to include tenant_id and the requested period, preventing cross-tenant and cross-period collisions. Redis failures are now non-fatal and fall back to the database.

2. Database connection pool was unusable

Files: database_pool.py, requirements.txt

Four independent issues prevented database access:

  • The connection URL referenced undefined settings (supabase_db_user, _password, _host, _port, _name) instead of the existing settings.database_url.

  • QueuePool, a synchronous pool, was passed to create_async_engine, which rejects it.

  • get_session was a plain async def, while callers used async with.

  • SQLAlchemy was installed without the asyncio extra, leaving greenlet unavailable.

Fix: The engine now uses settings.database_url, normalized to the postgresql+asyncpg:// scheme. Pool sizing comes from configuration, the incompatible explicit pool class was removed, and get_session is now an @asynccontextmanager. Initialization is idempotent, protected by a lock, and re-raises errors instead of swallowing them. Dependencies now specify sqlalchemy[asyncio]>=2.0.0.

3. Database failures returned fabricated revenue

File: reservations.py

  • Root cause: calculate_total_revenue caught every exception and returned hardcoded mock_data keyed only by property_id, without tenant isolation. For example, it returned 1000.00 for prop-001, matching the incorrect result produced by the timezone defect.

  • Fix: Removed the mock data. Failures now raise RevenueDataUnavailable, which the API maps to HTTP 503 with a neutral error message.

A financial dashboard must never present invented numbers as verified revenue when the database is unavailable.

4. Monthly revenue calculations ignored property timezones

Files: reservations.py, dashboard.py

  • Symptom: Sunset’s March revenue was understated.

  • Root cause: Naive datetime boundaries were compared against a TIMESTAMPTZ column without considering properties.timezone. A reservation stored at 2024-02-29 23:30:00+00 corresponds to 2024-03-01 00:30 in Europe/Paris. Its 1250.000 was excluded from March, producing 1000.00 instead of 2250.00.

  • An additional issue was that calculate_monthly_revenue was an unreachable stub returning Decimal('0'), with its query commented out and no tenant_id parameter.

Fix: The query now converts reservation timestamps using each property's timezone and applies a half-open monthly interval. The monthly calculation is implemented, tenant-scoped, and exposed through optional month and year query parameters. Inputs are validated (month: 1–12; year: 2000–2100), and requests supplying only one parameter are rejected with HTTP 400.

Using each property's timezone avoids correcting one client's totals while breaking another's.

5. Monetary calculations used binary floating point

Files: dashboard.py, RevenueSummary.tsx

  • Symptom: Finance reported revenue discrepancies of a few cents.

  • Root cause: The API converted Decimal values to float, and the frontend rounded using Math.round(data.total_revenue * 100) / 100. Binary floating-point representation can place values just below their intended midpoint, causing half-cent values such as 0.005 to round down to 0.00.

  • Fix: Revenue remains a Decimal and is quantized once using explicit ROUND_HALF_UP. The API serializes the result as a string, and the frontend now types total_revenue as string and formats it without converting it to a floating-point number.

6. Unknown users could be assigned a real tenant

Files: tenant_resolver.py, dashboard.py, secureApi.ts

  • Root cause: resolve_tenant_id defaulted unknown accounts to "tenant-a". The dashboard endpoint had a corresponding default_tenant fallback, potentially exposing Sunset's live revenue to unrecognized users.

  • Fix: Tenant resolution now returns None for unknown accounts, and the endpoint responds with HTTP 403 instead of guessing. The client-supplied X-Simulated-Tenant header was also removed from secureApi.ts. The backend derives the tenant from the bearer token server-side.

Verification results

All figures were verified against the live application using docker compose, covering both tenants after the fixes.

|
Property

|

Sunset (tenant-a)

|

Ocean (tenant-b)

|
| --- | --- | --- |
|

prop-001

|

2250.00 / 4

|

0.00 / 0

|
|

prop-002

|

4975.50 / 4

|

—

|
|

prop-003

|

6100.50 / 2

|

—

|
|

prop-004

|

0.00 / 0

|

1776.50 / 4

|
|

prop-005

|

—

|

3256.00 / 3

|

Additional checks passed:

  • prop-001, March 2024: 2250.00 / 4 reservations; February 2024: 0.00 / 0.

  • Side-by-side SQL comparison confirmed march_naive_utc = 1000.000 versus march_property_tz = 2250.000.

  • Nine tenant-scoped Redis keys verified, with no cross-tenant collisions.

  • Stopping the database using docker compose stop db resulted in HTTP 503 with a neutral message. Service recovered after restart through pool_pre_ping.

  • Unknown accounts received HTTP 403, including requests with a validly signed JWT claiming tenant_id: tenant-a.

  • Ocean continued receiving its own 0.00 when sending X-Simulated-Tenant: tenant-a.

  • npx tsc --noEmit completed successfully.

Out of scope: Issues flagged for follow-up

The following issues were identified but deliberately left unchanged because they were outside the scope of these fixes:

  • Database RLS: database/schema.sql enables row-level security but defines no policies. Tenant isolation currently depends on application-level enforcement.

  • Hardcoded secret: SECRET_KEY is set to debug_challenge_secret in docker-compose.yml. The current isolation checks assume trustworthy token signing, but this key is not suitable for production.

  • Hardcoded frontend properties: Dashboard.tsx contains a non-tenant-scoped PROPERTIES list and currently relies on the API returning zeros for properties a tenant does not own.

  • Legacy health endpoint: GET /health reports degraded due to a stubbed Supabase-style client in backend/app/database.py. The dashboard path does not use this component, so it was left untouched.

  • Missing .gitignore: __pycache__ and node_modules appear as untracked files.

Both tenants seed a property with id prop-001, so a key of
revenue:{property_id} let whichever tenant loaded first serve its
totals to the other for the 300s TTL.
DatabasePool read settings.supabase_db_* which config.py never
defines, so initialize() swallowed an AttributeError and left
session_factory as None. Also drops QueuePool (invalid for
create_async_engine), makes get_session an async context manager to
match its call sites, and adds the sqlalchemy[asyncio] extra so
greenlet is present.
calculate_total_revenue caught every DB error and returned a
hardcoded, tenant-blind mock_data table, so the dashboard showed
invented figures while the connection pool was broken. Errors now
raise RevenueDataUnavailable and surface as 503.
Month boundaries were naive UTC datetimes compared against
TIMESTAMPTZ, ignoring properties.timezone. A 2024-02-29 23:30Z
check-in is 1 March in Europe/Paris, so 1250.00 fell out of March and
prop-001 reported 1000.00 instead of 2250.00. calculate_monthly_revenue
was also a stub with no tenant_id parameter; it now executes and is
reachable via optional month/year query params.
total_amount is NUMERIC(10,3), so a total can land on a half cent.
float(total) on the API and Math.round(x*100)/100 on the client both
resolved those downwards: 0.005 displayed as 0.00. Totals are now
quantized once with ROUND_HALF_UP and sent as an exact string.
resolve_tenant_id returned 'tenant-a' for any unknown user and the
dashboard fell back to 'default_tenant', so an unrecognised account
received Sunset Properties' revenue. Both now refuse to guess and the
endpoint returns 403. Also removes the client-controlled
X-Simulated-Tenant header the backend rightly ignored.
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