Skip to content

fix: isolate tenant revenue data, apply property timezones and exact decimal totals - #217

Open
liberto6 wants to merge 4 commits into
Base360-AI:mainfrom
liberto6:main
Open

liberto6 wants to merge 4 commits into
Base360-AI:mainfrom
liberto6:main

Conversation

@liberto6

@liberto6 liberto6 commented Oct 9, 2026

Copy link
Copy Markdown

Fix revenue dashboard: tenant data leak, timezone month bounds and cent-level precision

Debugging fixes only. Every change maps to a reported symptom; no frontend changes, no dependency changes, no new endpoints.

# Reported symptom Root cause Fix
0 (hidden) Dashboard never showed real data DB pool built its URL from supabase_db_* settings that do not exist; every query failed and a hardcoded mock dict keyed only by property_id was returned Pool built from settings.database_url with asyncpg; mock fallback removed so DB errors surface
1 Ocean Rentals sees another company's revenue after refresh Cache key revenue:{property_id} ignores the tenant and prop-001 exists for both tenants; unresolved tenants defaulted to tenant-a Tenant in the cache key; unresolved tenant returns 403
2 Sunset Properties March totals do not match calculate_monthly_revenue was a stub returning 0 and used naive UTC month bounds, ignoring properties.timezone (res-tz-1 checks in 2024-02-29 23:30 UTC = 2024-03-01 00:30 Paris) Month bounds evaluated in the property's local timezone via AT TIME ZONE p.timezone
3 Finance: totals off by a few cents 3-decimal NUMERIC amounts were never rounded, then converted to float and rounded again in the browser Decimal rounded once in the backend with ROUND_HALF_UP

Commits

  1. fix: query the real database instead of mock data
  2. fix: isolate revenue cache and tenant resolution per tenant
  3. fix: compute monthly revenue in the property's timezone
  4. fix: round revenue totals to cents half-up once

Verification

  • backend/tests/ (10 tests, written test-first) run against the real schema and seed; Redis is faked.
  • Live check with both client accounts after flushing Redis: Sunset prop-001 = 2250.00 (4 reservations), Ocean prop-001 = 0.00 (0 reservations).

Run the tests inside the stack:

docker-compose exec backend pip install pytest pytest-asyncio fakeredis
docker-compose exec -e TEST_DATABASE_URL=postgresql://postgres:postgres@db:5432/propertyflow backend pytest -q tests

The DB pool built its URL from nonexistent supabase_db_* settings, so every
query failed and calculate_total_revenue silently returned hardcoded mock
totals keyed only by property_id. Build the asyncpg pool from
settings.database_url, share it, and let DB errors surface.
The revenue cache key was revenue:{property_id}, and prop-001 exists in two
tenants, so one client could see the other's cached totals. Unresolved
tenants also defaulted to tenant-a. Key the cache by tenant and reject
users without a tenant with 403.
calculate_monthly_revenue used naive UTC month bounds and returned a
placeholder 0. Query the real data and evaluate month bounds in each
property's local timezone, so res-tz-1 (Feb 29 23:30 UTC) counts in March
for a Paris property.
Amounts are NUMERIC(10,3) and the total was returned unrounded. Round the
Decimal total once with ROUND_HALF_UP so cent-level totals match finance.
Comment on lines +91 to +92
# Unknown users must never be silently assigned to another client's tenant.
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The change to resolve_tenant_id can result in user.tenant_id being None. The list_departments endpoint doesn't filter by tenant in this case, leaking all departments.
Severity: HIGH

Suggested Fix

The authenticate_request and verify_token_ws functions in auth.py should ensure a valid tenant ID is always resolved for authenticated users, or endpoints like list_departments should explicitly reject requests where user.tenant_id is None instead of querying without a filter.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: backend/app/core/tenant_resolver.py#L91-L92

Potential issue: The `TenantResolver.resolve_tenant_id` function now returns `None` for
any authenticated user not matching three specific emails. Previously, these users
received a default tenant ID. Endpoints like `GET /departments` are not prepared for a
`None` tenant ID. The `list_departments` function only applies its tenant filter if
`user.tenant_id` is truthy. For a user with a `None` tenant ID (like an admin), the
query runs unfiltered, returning departments from all tenants. This introduces a new
cross-tenant data leak. Other endpoints may fail or write incorrect data.

Did we get this right? 👍 / 👎 to inform future reviews.

Comment on lines 19 to +29
"""

# In production this query executes against a database session.
# result = await db.fetch_val(query, property_id, tenant_id, start_date, end_date)
# return result or Decimal('0')

return Decimal('0') # Placeholder for now until DB connection is finalized

from app.core.database_pool import db_pool

if db_pool.pool is None:
await db_pool.initialize()

async with db_pool.get_session() as conn:
total = await conn.fetchval(query, property_id, tenant_id, year, month)

return Decimal(str(total or 0)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The new calculate_monthly_revenue function is not called by the dashboard endpoint it's intended to fix, leaving the original timezone bug in place. The fix is dead code.
Severity: MEDIUM

Suggested Fix

Update get_revenue_summary in dashboard.py to call the new calculate_monthly_revenue function instead of calculate_total_revenue. This will apply the intended timezone-aware, monthly revenue calculation to the dashboard summary.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: backend/app/services/reservations.py#L4-L29

Potential issue: The new timezone-aware function `calculate_monthly_revenue` is not
actually used by any production code path. The `/dashboard/summary` endpoint, which this
change is intended to fix, still calls the old `get_revenue_summary` function, which in
turn uses `calculate_total_revenue`. This old function does not filter by month or
handle timezones. As a result, the dashboard's revenue calculation remains incorrect and
the fix for the reported timezone issue is not applied.

Did we get this right? 👍 / 👎 to inform future reviews.

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