# P0 Root Cause Analysis: POST /api/v1/expenses/ 500 Internal Server Error **Date:** 2026-07-26 **Gitea Issue:** [#421](https://gitea.kincses.dev/kincses/service-finder/issues/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 ```mermaid 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 ```python # 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`: ```python 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 ```python # 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`: ```python 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 ```python .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: ```python # 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: ```diff - from app.models.fleet.org_role import OrgRole + from app.models.marketplace.organization import OrgRole ``` **Change 2 โ€” Line 108:** Fix the join column: ```diff - .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: ```python # 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_expense` โ†’ `PENDING_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_role` โ†’ `app.models.marketplace.organization` | | **Root Cause 2** | Wrong column: `OrgRole.name` โ†’ `OrgRole.name_key` | | **Gitea Issue** | #421 | | **Fix Complexity** | Trivial (2 lines) | | **Risk** | Minimal | --- *Analysis by: Service Finder Rendszer-Architect โ€” 2026-07-26*