Files
service-finder/docs/admin_garage_subscription_tab_bugfix_spec.md

316 lines
10 KiB
Markdown

# 🔧 Javítási Terv: Admin Garázs Előfizetés Fül — Hibás Csomagadatok
> **Gitea Issue:** [#301](http://192.168.100.10:3000/kincses/service-finder/issues/301)
> **Hatáskör:** Backend (admin_organizations.py) + Frontend (index.vue)
> **Típus:** Bug
> **Prioritás:** Magas
---
## 📋 Összefoglaló
A frontend admin `frontend_admin/pages/garages/[id]/index.vue` garázs részletek oldal Előfizetés (Subscription) fülén a kiválasztott csomaghoz tartozó adatok (asset_limit, branch_limit, asset_count) nem a valóságot tükrözik. A backend `SubscriptionSummary` Pydantic modellje hiányos, és a limit-ek kinyerése törékeny.
---
## 🔍 Feltárt Hibák (Root Causes)
### 🐛 #1: `branch_limit` mező hiányzik a `SubscriptionSummary`-ból
**Hely:** `backend/app/api/v1/endpoints/admin_organizations.py:116-124`
A `SubscriptionSummary` Pydantic modell jelenleg nem tartalmaz `branch_limit` mezőt. A frontend template azonban használja:
```typescript
// frontend_admin/pages/garages/[id]/index.vue:526
{{ garage.branches?.length || 0 }} / {{ garage.subscription?.branch_limit || '∞' }}
```
Mivel `branch_limit` sosem érkezik a backend válaszban, a mező `undefined`, és a frontend mindig `'∞'`-t (végtelen) jelenít meg.
### 🐛 #2: `asset_limit` kinyerése logikai hibás
**Hely:** `backend/app/api/v1/endpoints/admin_organizations.py:462-465`
```python
asset_limit = (
rules.get("max_vehicles")
or active_sub.tier.feature_capabilities.get("max_vehicles")
or 1
)
```
**Problémák:**
1. Python `or` operátor falsy-nak tekinti a `0`-t — ha a tier `max_vehicles: 0`, az `or` miatt `feature_capabilities`-re ugrik tovább.
2. Ha `feature_capabilities`-ben sincs `max_vehicles`, vagy az is `0`, az `asset_limit = 1` lesz.
3. Ha a tier más kulcsnevet használ (pl. `vehicle_limit`, `max_assets`), az érték hibásan `1`-re esik vissza.
### 🐛 #3: `GarageDetailsResponse` nem tartalmaz `branches` mezőt
**Hely:** `backend/app/api/v1/endpoints/admin_organizations.py:127-175`
A `GarageDetailsResponse` modellben nincs `branches` lista. A frontend `garage.branches?.length` használ, de a backend nem ad vissza branch adatokat, így a kihasználtsági mutató mindig `0 / ∞`.
### 🐛 #4: `asset_count` hardkódolt (mindig 0)
**Hely:** `backend/app/api/v1/endpoints/admin_organizations.py:473`
```python
asset_count=0,
```
Az `asset_count` mindig `0`, sosem számolja le a szervezet tényleges járműveit.
### 🐛 #5: Lejárt előfizetés is aktívként jelenhet meg
**Hely:** `backend/app/api/v1/endpoints/admin_organizations.py:414-425`
```python
.where(
OrganizationSubscription.org_id == org_id,
OrganizationSubscription.is_active == True,
)
```
A lekérdezés nem ellenőrzi a `valid_until >= now()` feltételt. Ha egy subscription `is_active=True` de `valid_until` már lejárt, a backend továbbra is aktívként kezeli.
### 🐛 #6: Fallback path hiányos adatokkal
**Hely:** `backend/app/api/v1/endpoints/admin_organizations.py:476-488`
Ha nincs aktív `OrganizationSubscription`, a kód a `org.subscription_tier` denormalizált kapcsolatra esik vissza. Ez a path nem adja vissza a `valid_from` és `is_active` mezőket, csak a defaultokat használja.
---
## 🛠️ Javítási Terv
### 1. Backend: `SubscriptionSummary` bővítése
**Fájl:** `backend/app/api/v1/endpoints/admin_organizations.py`
#### 1/a. `branch_limit` mező hozzáadása (sor 116-124 körül)
A `SubscriptionSummary` osztály bővítése:
```python
class SubscriptionSummary(BaseModel):
tier_name: str = "Free/Fallback"
tier_level: int = 0
valid_from: Optional[str] = None
expires_at: Optional[str] = None
is_active: bool = True
asset_count: int = 0
asset_limit: int = 1
branch_limit: int = 0 # 🆕 HIÁNYZÓ MEZŐ
```
#### 1/b. `asset_limit` kinyerés javítása (sor 462-465)
Az `or` helyett használj **explicit `max()` logikát**:
```python
# JAVÍTVA: explicit max() használata az or helyett
rules = active_sub.tier.rules or {}
fc = active_sub.tier.feature_capabilities or {}
# Kompatibilitás: több kulcsnév támogatása
vehicle_limit_keys = ["max_vehicles", "vehicle_limit", "max_assets"]
asset_limit = max(
(rules.get(k, 0) for k in vehicle_limit_keys),
default=0
)
asset_limit = max(
asset_limit,
max((fc.get(k, 0) for k in vehicle_limit_keys), default=0),
)
asset_limit = max(asset_limit, 1) # minimum 1
```
**Ugyanezt a javítást kell elvégezni a fallback path-on is** (sor 477-482).
#### 1/c. `branch_limit` kitermelése a rules-ből (sor 462-488 között)
```python
# 🆕 HIÁNYZÓ: branch_limit kinyerése
branch_limit_keys = ["max_branches", "branch_limit", "max_garages"]
branch_limit = max(
(rules.get(k, 0) for k in branch_limit_keys),
default=0
)
branch_limit = max(
branch_limit,
max((fc.get(k, 0) for k in branch_limit_keys), default=0),
)
```
Ezt add hozzá mindkét path-hoz (aktív sub + fallback):
```python
subscription_summary = SubscriptionSummary(
...
asset_limit=asset_limit,
branch_limit=branch_limit, # 🆕
)
```
#### 1/d. `asset_count` dinamikus számolása
Az `asset_count`-ot ne hardkódold. Számold meg a szervezet járműveit. Ehhez be kell tölteni az `Organization.assets` relationship-et a SELECT-nél:
```python
org_stmt = (
select(Organization)
.options(
selectinload(Organization.subscription_tier),
selectinload(Organization.owner).selectinload(User.person),
selectinload(Organization.assets), # 🆕
)
.where(Organization.id == org_id)
)
```
Majd a subscription summary építésénél:
```python
asset_count = len(org.assets) if hasattr(org, 'assets') and org.assets else 0
```
VAGY használj explicit lekérdezést:
```python
from sqlalchemy import func
from app.models.asset import AssetAssignment
asset_count_stmt = select(func.count(AssetAssignment.id)).where(
AssetAssignment.organization_id == org_id
)
asset_count_result = await db.execute(asset_count_stmt)
asset_count = asset_count_result.scalar() or 0
```
#### 1/e. Lejárt előfizetés szűrés (sor 414-425)
```python
from sqlalchemy import or_ as sa_or
sub_stmt = (
select(OrganizationSubscription)
.options(selectinload(OrganizationSubscription.tier))
.where(
OrganizationSubscription.org_id == org_id,
OrganizationSubscription.is_active == True,
# 🆕 HIÁNYZÓ: lejárati dátum ellenőrzése
sa_or_(
OrganizationSubscription.valid_until.is_(None),
OrganizationSubscription.valid_until >= func.now(),
),
)
.order_by(OrganizationSubscription.id.desc())
.limit(1)
)
```
### 2. Backend: `GarageDetailsResponse` bővítése `branches` listával
**Fájl:** `backend/app/api/v1/endpoints/admin_organizations.py`
#### 2/a. BranchBrief Pydantic modell létrehozása (a SubscriptionSummary után)
```python
class BranchBrief(BaseModel):
"""Fióktelep rövid adatai."""
id: int
name: str
city: Optional[str] = None
is_active: bool = True
```
#### 2/b. `GarageDetailsResponse` bővítése `branches`-szel
```python
class GarageDetailsResponse(BaseModel):
...
subscription: Optional[SubscriptionSummary] = None
branches: List[BranchBrief] = [] # 🆕 HIÁNYZÓ MEZŐ
primary_contact: Optional[ContactPersonInfo] = None
...
```
#### 2/c. Branch adatok lekérdezése a végpontban
```python
# 🆕 HIÁNYZÓ: branch adatok lekérése
from app.models.location import Branch
branches_stmt = select(Branch).where(Branch.organization_id == org_id)
branches_result = await db.execute(branches_stmt)
branches_data = branches_result.scalars().all()
branch_list = [
BranchBrief(
id=b.id,
name=b.name,
city=b.city,
is_active=b.is_active,
)
for b in branches_data
]
```
### 3. Frontend: Ellenőrző módosítások
**Fájl:** `frontend_admin/pages/garages/[id]/index.vue`
#### 3/a. `branchQuotaPercent` computed property ellenőrzése
Ellenőrizd, hogy a számított property helyesen működik a nem-`undefined` `branch_limit`-nel:
```typescript
const branchQuotaPercent = computed(() => {
if (!garage.value?.subscription?.branch_limit) return 0
const branchCount = garage.value?.branches?.length || 0
return (branchCount / garage.value.subscription.branch_limit) * 100
})
```
---
## ✅ Elfogadási Kritériák (Acceptance Criteria)
1. **Branch limit megjelenítése:** A branches utilization sorban a backend által küldött `branch_limit` érték jelenjen meg (ne mindig `∞`).
2. **Asset limit helyes értéke:** A kiválasztott csomag `rules` JSONB-jében lévő `max_vehicles` (vagy alternatív kulcs) értéke jelenjen meg.
3. **Asset count valós adat:** A járművek száma ne `0` legyen, hanem a tényleges asset-ek száma.
4. **Branch count valós adat:** A branch-ek száma ne mindig `0` legyen, hanem a tényleges branch-ek száma a `branches` listából.
5. **Lejárt előfizetés:** Ne jelenjen meg aktív előfizetésként, ha a `valid_until` már lejárt.
6. **Fallback:** Ha nincs aktív subscription, a `org.subscription_tier` denormalizált mezőből származó adatok legyenek teljesek (valid_from, is_active).
---
## 📦 Módosítandó Fájlok
| # | Fájl | Változtatás |
|---|------|-------------|
| 1 | `backend/app/api/v1/endpoints/admin_organizations.py` | `SubscriptionSummary` bővítése `branch_limit`-lel, `asset_limit` logika javítása, `asset_count` dinamikus számolása, `GarageDetailsResponse` bővítése `branches`-szel + `BranchBrief`, lejárati dátum szűrés |
| 2 | `frontend_admin/pages/garages/[id]/index.vue` | `branchQuotaPercent` számítás ellenőrzése |
---
## 🧪 Tesztelési Terv
1. **Létező garázs megnyitása:** Ellenőrizd, hogy az Előfizetés fül a tényleges csomag adatait mutatja.
2. **Csomagváltás:** Válts csomagot, és ellenőrizd, hogy a frissítés után a helyes adatok jelennek meg.
3. **Lejárt előfizetés:** Állíts be egy lejárt `valid_until` dátumot, és ellenőrizd, hogy a garázs fallback csomag adatait mutatja.
4. **Több branch-es garázs:** Ellenőrizd, hogy a branch count és limit helyesen jelenik meg.
5. **API teszt:** `GET /api/v1/admin/organizations/{id}/details` - ellenőrizd, hogy a válasz tartalmazza a `subscription.branch_limit` és `branches` mezőket.
---
## ⚡ Végrehajtási Sorrend
1. **Code mód** váltása
2. Backend módosítások elvégzése (`admin_organizations.py`)
3. Sync engine futtatása: `docker compose exec sf_api python3 /app/backend/app/scripts/sync_engine.py`
4. API konténer újraindítása
5. Frontend (ha szükséges) módosítása
6. Manuális tesztelés a böngészőből