Files
service-finder/docs/p0_expense_500_import_bugfix_2026-07-26.md
2026-07-27 08:39:18 +00:00

6.5 KiB

P0 Root Cause Analysis: POST /api/v1/expenses/ 500 Internal Server Error

Date: 2026-07-26 Gitea Issue: #421 Severity: P0 (Critical — breaks entire expense creation flow) Status: Architect Analysis Complete — Awaiting Code Fix


📋 Executive Summary

The POST /api/v1/expenses/ endpoint crashes with a 500 Internal Server Error because the helper function _check_org_capability() contains a broken import path and a wrong join column name. Both defects are located in backend/app/api/v1/endpoints/expenses.py.


🔍 Root Cause Trace

Traceback (from docker compose logs sf_api)

File "/app/app/api/v1/endpoints/expenses.py", line 377, in create_expense
    can_approve = await _check_org_capability(db, current_user.id, organization_id, "can_approve_expense")
File "/app/app/api/v1/endpoints/expenses.py", line 104, in _check_org_capability
    from app.models.fleet.org_role import OrgRole
ModuleNotFoundError: No module named 'app.models.fleet.org_role'

Call Flow

flowchart TD
    A["POST /api/v1/expenses/"] --> B["expenses.py: create_expense() line 377"]
    B --> C["_check_org_capability() line 93"]
    C --> D["line 104: from app.models.fleet.org_role import OrgRole"]
    D --> E["ModuleNotFoundError 💥"]
    E --> F["500 Internal Server Error"]

🐛 Bug #1: Wrong Import Path (line 104)

Broken Code

# File: backend/app/api/v1/endpoints/expenses.py, line 104
from app.models.fleet.org_role import OrgRole

Why It Fails

There is no directory backend/app/models/fleet/ and no file org_role.py at that path.

Correct Import

The OrgRole model lives in backend/app/models/marketplace/organization.py:

from app.models.marketplace.organization import OrgRole

Evidence: Other Files Import Correctly

File Import Status
backend/app/api/deps.py:13 from app.models.marketplace.organization import OrgRole, OrganizationMember Correct
backend/app/api/v1/endpoints/assets.py:14 from app.models import ... OrgRole (via __init__.py) Correct
backend/app/api/v1/endpoints/organizations.py:23 from app.models.marketplace.organization import ... OrgRole Correct
backend/app/api/v1/endpoints/expenses.py:104 from app.models.fleet.org_role import OrgRole BROKEN

🐛 Bug #2: Wrong Join Column (line 108)

Broken Code

# File: backend/app/api/v1/endpoints/expenses.py, lines 106-114
stmt = (
    select(OrgRole.permissions)
    .join(OrganizationMember, OrganizationMember.role == OrgRole.name)  # ← BUG
    .where(
        OrganizationMember.user_id == user_id,
        OrganizationMember.organization_id == organization_id,
        OrganizationMember.status == "active",
    )
)

Why It Fails

The OrgRole model's column is name_key (line 55 of the model), not name:

class OrgRole(Base):
    __tablename__ = "org_roles"
    __table_args__ = {"schema": "fleet"}

    name_key: Mapped[str] = mapped_column(String(50), unique=True, nullable=False, index=True)
    # ... no 'name' column exists

Attempting OrgRole.name would raise an AttributeError after the import is fixed, causing a second crash.

Correct Join

.join(OrganizationMember, OrganizationMember.role == OrgRole.name_key)

Evidence: assets.py Has the Correct Pattern

The assets.py endpoint uses the identical capability-check pattern correctly:

# assets.py line 80-83 — CORRECT pattern
role_stmt = select(OrgRole.permissions).where(
    OrgRole.name_key == org_role_name,   # ← Uses name_key, not .name
    OrgRole.is_active == True
).limit(1)

🧩 Impact Analysis

What's Broken

  • Entire POST /api/v1/expenses/ endpoint — all expense creation requests fail with 500
  • The crash occurs at the RBAC Phase 2 capability check (JSONB-based can_approve_expense)
  • This blocks the P0 Smart Expense Workflow (odometer normalization, VAT calculation, gamification hooks, AssetEvent linking)

What Still Works

  • GET /api/v1/expenses/ — list all expenses (does not call _check_org_capability)
  • GET /api/v1/expenses/{asset_id} — list asset expenses (does not call _check_org_capability)
  • PUT /api/v1/expenses/{expense_id} — update expense (does not call _check_org_capability)

Why This Was Not Caught

  1. The function uses a lazy import inside the function body — the import error only surfaces at runtime, not at module load time.
  2. No e2e test exercises the POST /expenses/ path through this function.
  3. The backend/app/tests/e2e/test_expense_flow.py test targets POST /api/v1/expenses/add, which is a different, legacy endpoint — not the current POST /api/v1/expenses/.

🔧 Fix Plan (2 Lines, 1 File)

File: backend/app/api/v1/endpoints/expenses.py

Change 1 — Line 104: Replace the broken import:

-    from app.models.fleet.org_role import OrgRole
+    from app.models.marketplace.organization import OrgRole

Change 2 — Line 108: Fix the join column:

-        .join(OrganizationMember, OrganizationMember.role == OrgRole.name)
+        .join(OrganizationMember, OrganizationMember.role == OrgRole.name_key)

Optional: Hoist to top-level imports

Since OrganizationMember is already imported at the top of the file (line 46), move OrgRole there too:

# line 46 area — add OrgRole
from app.models.marketplace.organization import OrganizationMember, OrgRole

Then remove the lazy import body from _check_org_capability().


🧪 Verification

  1. Unit test: POST /api/v1/expenses/ with valid payload → 201 Created
  2. Capability check: User with can_approve_expense: true → expense APPROVED
  3. Capability check: User without can_approve_expensePENDING_APPROVAL
  4. No ModuleNotFoundError at any point

📊 Summary

Item Detail
Endpoint POST /api/v1/expenses/
Error ModuleNotFoundError: No module named 'app.models.fleet.org_role'
Crash File backend/app/api/v1/endpoints/expenses.py
Crash Line 104
Root Cause 1 Wrong import: app.models.fleet.org_roleapp.models.marketplace.organization
Root Cause 2 Wrong column: OrgRole.nameOrgRole.name_key
Gitea Issue #421
Fix Complexity Trivial (2 lines)
Risk Minimal

Analysis by: Service Finder Rendszer-Architect — 2026-07-26