# 🔧 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