felhasználói felületre pü
This commit is contained in:
203
docs/p0_expense_500_import_bugfix_2026-07-26.md
Normal file
203
docs/p0_expense_500_import_bugfix_2026-07-26.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# 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*
|
||||
Reference in New Issue
Block a user