admin felület különválasztva

This commit is contained in:
Roo
2026-06-24 11:29:45 +00:00
parent 71ef33bb85
commit 80a5d67f79
462 changed files with 87873 additions and 312 deletions

View File

@@ -0,0 +1,190 @@
# P0 READ & COMPARE — Vendor and Expense Architecture Audit Report
**Date:** 2026-06-23
**Author:** Fast Coder (Core Developer)
**Scope:** Vendor creation flow + Expense creation flow
**Status:** COMPLETE — No code modified, pure audit
---
## 1. AUDIT: Vendor Creation Flow
### 1.1. Endpoint: `POST /providers/quick-add`
**File:** [`backend/app/api/v1/endpoints/providers.py:241`](../backend/app/api/v1/endpoints/providers.py:241)
This is the **primary** vendor creation endpoint used by the frontend's CostEntryWizard when a user adds a new service provider (vendor) via crowdsourcing.
**Actual Code Flow** (in [`backend/app/services/provider_service.py:468`](../backend/app/services/provider_service.py:468)):
| Step | What happens | Line |
|------|-------------|------|
| 1 | Organization created with `org_type=OrgType.service_provider` | `:526` |
| 2 | `owner_id=None` — explicitly set to None | `:541` |
| 3 | `status="pending_verification"`, `is_verified=False` | `:527-528` |
| 4 | ServiceProfile created with `status="ghost"` | `:561-570` |
| 5 | Branch created with address data | `:618-630` |
| 6 | **OrganizationMember created with `role=OrgUserRole.ADMIN`** | `:639-646` |
### 1.2. Comparison: Actual vs Expected
| Aspect | Expected (Business Logic) | Actual Code | Verdict |
|--------|--------------------------|-------------|---------|
| `org_type` | `SERVICE_PROVIDER` | `OrgType.service_provider` ✅ | **CORRECT** |
| `owner_id` | `NULL` (ideally) | `owner_id=None` ✅ | **CORRECT** |
| Creator in members? | **NO** — creator should NOT be a member | **BUG:** Creator IS added as `OrganizationMember` with `role=ADMIN` ❌ | **DEVIATION** |
### 1.3. The Member Problem
In [`backend/app/services/provider_service.py:639-646`](../backend/app/services/provider_service.py:639-646):
```python
member = OrganizationMember(
organization_id=org.id,
user_id=user_id,
role=OrgUserRole.ADMIN, # <-- ADMIN, not OWNER
is_permanent=True,
is_verified=True,
)
```
The code comment at line 635 says:
> *"A létrehozó felhasználó ADMIN szerepkörű tag lesz (NEM OWNER). Crowdsourcingból felvett szolgáltatóknak nincs tulajdonosa (owner_id=NULL). Az ADMIN jogosultság elegendő a későbbi szerkesztéshez, de a cég nem jelenik meg a 'Cégeim' garázsválasztóban."*
**However**, the expected business logic says:
> *"A létrehozó felhasználó NEM kerülhet be a tagok (organization_members) közé!"*
This is a **deliberate design decision** — the current code adds the creator as ADMIN. The expected logic says NO membership at all. This needs Architect clarification.
### 1.4. Secondary Endpoint: `POST /organizations/onboard`
**File:** [`backend/app/api/v1/endpoints/organizations.py:38`](../backend/app/api/v1/endpoints/organizations.py:38)
This endpoint is for **business onboarding** (not vendor creation). It creates:
- `org_type=OrgType.business` (line 84)
- `owner_id` is NOT set (implicitly NULL)
- Creator is added as `OrganizationMember` with `role="OWNER"` (line 116-121)
This is **correct** for business onboarding — the creator SHOULD be the owner.
---
## 2. AUDIT: Expense Creation Flow
### 2.1. Endpoint: `POST /expenses/`
**File:** [`backend/app/api/v1/endpoints/expenses.py:345`](../backend/app/api/v1/endpoints/expenses.py:345)
### 2.2. How `organization_id` is Resolved
**Actual Code** (lines 398-429):
```python
# CRITICAL FIX: organization_id MUST be the user's active organization context,
# NOT blindly taken from the payload.
#
# Resolution strategy:
# 1. First, try to resolve the user's active organization from their membership
# 2. Fallback to asset.current_organization_id or asset.owner_org_id
# 3. Ignore expense.organization_id from payload to prevent vendor-org pollution
org_stmt = select(OrganizationMember).where(
OrganizationMember.user_id == current_user.id,
OrganizationMember.status == "active"
).limit(1)
org_result = await db.execute(org_stmt)
org_member = org_result.scalar_one_or_none()
if org_member:
organization_id = org_member.organization_id # Priority 1
elif asset.current_organization_id:
organization_id = asset.current_organization_id # Priority 2
elif asset.owner_org_id:
organization_id = asset.owner_org_id # Priority 3
else:
# B2C fallback: any membership (even unverified)
...
```
### 2.3. How `vendor_organization_id` is Set
**Actual Code** (line 495):
```python
vendor_organization_id=expense.vendor_organization_id,
```
The payload's `vendor_organization_id` is passed **directly** to the model. This is the vendor's organization ID.
### 2.4. Comparison: Actual vs Expected
| Aspect | Expected (Business Logic) | Actual Code | Verdict |
|--------|--------------------------|-------------|---------|
| `organization_id` source | Asset's garage (`asset.current_organization_id`) | User's active org membership (Priority 1), then `asset.current_organization_id` (Priority 2) | **PARTIAL DEVIATION** |
| Payload `organization_id` | Should be **IGNORED** | IS ignored — backend resolves from user context ✅ | **CORRECT** |
| `vendor_organization_id` | The invoice issuer (vendor) | Set from `expense.vendor_organization_id` ✅ | **CORRECT** |
### 2.5. The organization_id Deviation Explained
The expected logic says:
> *"A költség organization_id mezőjének (a költség gazdájának) a jármű saját garázsát (asset.current_organization_id) kell tükröznie."*
The actual code uses **Priority 1: user's active org membership**. This means if a user belongs to Organization A (their fleet manager org) but the asset is currently assigned to Organization B (a different garage), the expense will be saved under Organization A, not Organization B.
**This is a potential bug** in multi-org scenarios where a user manages vehicles across multiple organizations. The asset's `current_organization_id` should be the authoritative source.
### 2.6. Frontend Payload Analysis
**File:** [`frontend/src/components/cost/CostEntryWizard.vue:690-723`](../frontend/src/components/cost/CostEntryWizard.vue:690)
The frontend:
1. Does **NOT** send `vendor_organization_id` in the payload (line 690-714)
2. Sends `organization_id` = user's `active_organization_id` (line 720-723)
3. Stores vendor info inside `data.vendor_id` and `data.vendor_name` (JSONB) (line 707-710)
**File:** [`frontend/src/stores/cost.ts:64-73`](../frontend/src/stores/cost.ts:64)
The cost store also injects `organization_id` from `authStore.user?.active_organization_id`.
**This means:** The frontend never sends `vendor_organization_id` to the backend! The vendor info is stored in the JSONB `data` field, but the dedicated `vendor_organization_id` column on `AssetCost` remains NULL.
---
## 3. Summary of Findings
### 🟢 CORRECT (Matches Expected Logic)
| # | Finding | Location |
|---|---------|----------|
| 1 | `POST /providers/quick-add` creates org with `org_type=service_provider` | [`provider_service.py:526`](../backend/app/services/provider_service.py:526) |
| 2 | `owner_id=None` for crowdsourced providers | [`provider_service.py:541`](../backend/app/services/provider_service.py:541) |
| 3 | Backend `POST /expenses/` ignores payload `organization_id` | [`expenses.py:398-429`](../backend/app/api/v1/endpoints/expenses.py:398) |
| 4 | `vendor_organization_id` is correctly mapped from schema | [`expenses.py:495`](../backend/app/api/v1/endpoints/expenses.py:495) |
### 🟡 DEVIATION (Needs Architect Decision)
| # | Finding | Location | Impact |
|---|---------|----------|--------|
| 1 | **Vendor creator IS added as OrganizationMember (ADMIN)** — expected: NO membership | [`provider_service.py:639-646`](../backend/app/services/provider_service.py:639) | Vendor appears in user's org list? |
| 2 | **`organization_id` resolved from user's active org (Priority 1)** — expected: `asset.current_organization_id` | [`expenses.py:406-414`](../backend/app/api/v1/endpoints/expenses.py:406) | Wrong org in multi-org scenarios |
| 3 | **Frontend never sends `vendor_organization_id`** — vendor info only in JSONB `data` | [`CostEntryWizard.vue:690-714`](../frontend/src/components/cost/CostEntryWizard.vue:690) | `vendor_organization_id` column always NULL |
### 🔴 CRITICAL GAP
| # | Finding | Location |
|---|---------|----------|
| 1 | Frontend `CostEntryWizard` does NOT populate `vendor_organization_id` in the POST payload | [`CostEntryWizard.vue:707`](../frontend/src/components/cost/CostEntryWizard.vue:707) — vendor_id stored only in `data.vendor_id` |
| 2 | The `cost.ts` store also doesn't map `vendor_organization_id` | [`cost.ts:53-73`](../frontend/src/stores/cost.ts:53) |
---
## 4. Recommended Actions
1. **Architect Decision Needed:** Should `quick_add_provider` add the creator as `OrganizationMember` (current behavior) or NOT (expected logic)? The current ADMIN role prevents the org from appearing in "My Companies" garage selector, but the user still has a membership record.
2. **Fix `organization_id` Resolution:** Change Priority 1 from "user's active org membership" to `asset.current_organization_id` to match the expected business logic exactly.
3. **Fix Frontend:** Add `vendor_organization_id` to the POST `/expenses/` payload when a vendor is selected from the dropdown, so the dedicated column is populated instead of only storing it in JSONB `data`.
---
*End of Audit Report — No code was modified during this analysis.*