# P0 Architecture Plan — Subscription UI, Expiration Timers & Smart Ad Policy **Dátum:** 2026-06-18 **Szerző:** Fast Coder (Core Developer) **Státusz:** Tervezési fázis — Jóváhagyásra vár **Cél:** Teljes körű architekturális terv a Subscription UI (Frontend), Expiration Timers (Backend) és a dinamikus, tier-alapú Smart Ad Policy rendszerhez. --- ## 1. AUDIT: Backend Modellek — Előfizetés Időzítők és Állapot ### 1.1 Jelenlegi Adatmodell Áttekintés A rendszer három rétegben tárolja az előfizetési információkat: #### A) `system.subscription_tiers` — Csomagdefiníciók - **Modell:** [`SubscriptionTier`](backend/app/models/core_logic.py:12) - **Kulcs mezők:** `id`, `name` (unique), `rules` (JSONB), `is_custom` - **JSONB `rules` struktúra:** Lásd a [`seed_packages.py`](backend/app/scripts/seed_packages.py:37) definícióit - **Jelenlegi csomagok:** 10 db (4 private + 6 corporate) #### B) `finance.org_subscriptions` — Szervezeti előfizetés rekordok - **Modell:** [`OrganizationSubscription`](backend/app/models/core_logic.py:25) - **Kulcs mezők:** - `org_id` → FK `fleet.organizations.id` - `tier_id` → FK `system.subscription_tiers.id` - `valid_from` (DateTime, server_default=now) - **`valid_until`** (DateTime, nullable) — **EZ A LEJÁRATI IDŐ** - `is_active` (Boolean, default=True) #### C) `finance.user_subscriptions` — Felhasználói előfizetés rekordok - **Modell:** [`UserSubscription`](backend/app/models/core_logic.py:44) - **Kulcs mezők:** `user_id`, `tier_id`, `valid_from`, **`valid_until`**, `is_active`, `created_at`, `updated_at` #### D) `fleet.organizations` — Szervezet (denormalizált subscription adatok) - **Modell:** [`Organization`](backend/app/models/marketplace/organization.py:70) - **Kulcs mezők:** - `subscription_plan` (String, legacy — server_default='FREE') - `subscription_tier_id` (FK → `system.subscription_tiers.id`, nullable) - `base_asset_limit` (Integer, server_default=1) - `purchased_extra_slots` (Integer, server_default=0) ### 1.2 Lejárati Idő (valid_until) Jelenlegi Állapota | Tábla | Oszlop | Típus | Alapértelmezés | Jelenlegi Használat | |-------|--------|-------|----------------|---------------------| | `finance.org_subscriptions` | `valid_until` | `DateTime(timezone=True)` | `NULL` | **Nincs beállítva** a [`upgrade_org_subscription()`](backend/app/services/billing_engine.py:805) függvényben — csak `valid_from` kerül beállításra | | `finance.user_subscriptions` | `valid_until` | `DateTime(timezone=True)` | `NULL` | **Nincs beállítva** a felhasználói előfizetés létrehozáskor | | `identity.users` | `subscription_expires_at` | (a kódban használva) | — | A [`billing_engine.py:792`](backend/app/services/billing_engine.py:792) beállítja: `user.subscription_expires_at = datetime.utcnow() + timedelta(days=30)` | **Következtetés:** A `valid_until` mező jelenleg **nem töltődik be** a szervezeti előfizetés hozzárendelésekor. Ez egy hiányosság, amit pótolni kell. ### 1.3 Tervezett Módosítás: `GET /api/v1/organizations/my` Subscription Adatokkal A [`GET /my`](backend/app/api/v1/endpoints/organizations.py:180) végpont jelenleg **nem adja vissza** az előfizetési adatokat. Tervezett bővítés: ```python # Jelenlegi válasz (hiányos): { "organization_id": 1, "subscription_plan": "FREE", "subscription_tier_id": None, # ... nincs subscription info } # Tervezett bővítés: { "organization_id": 1, "subscription_plan": "corp_premium_v1", "subscription_tier_id": 16, "subscription": { "tier_id": 16, "tier_name": "corp_premium_v1", "display_name": "Céges Prémium", "valid_from": "2026-06-01T00:00:00Z", "valid_until": "2026-07-01T00:00:00Z", # LEJÁRAT "is_active": True, "allowances": { "max_vehicles": 20, "max_garages": 3, "monthly_free_credits": 300 }, "pricing": { "monthly_price": 29.99, "yearly_price": 299.99, "currency": "EUR" } } } ``` **Implementációs terv:** 1. A [`GET /my`](backend/app/api/v1/endpoints/organizations.py:180) végpontban minden Organization rekordhoz töltsük be a hozzá tartozó aktív `OrganizationSubscription` rekordot (JOIN vagy selectinload). 2. Ha van aktív subscription, adjuk vissza a `valid_until`, `tier.rules` (allowances, pricing, display_name) adatokat. 3. Ha nincs, a subscription blokk legyen `null`. --- ## 2. SMART AD POLICY — JSONB Architektúra ### 2.1 Tervezési Döntés A Tervező elvetette az egyszerű `hide_ads` boolean megközelítést. Ehelyett egy **3 szintű, tier-alapú hirdetési politika** kerül bevezetésre, amely a `system.subscription_tiers.rules` JSONB oszlopba ágyazva él. ### 2.2 Az `ad_policy` JSON Struktúra ```json { "ad_policy": { "external_display": { "enabled": true | false, "networks": ["google_ads", "taboola", "outbrain"], "max_per_page": 3, "placement": ["sidebar", "between_cards", "footer"] }, "internal_upsell": { "enabled": true | false, "show_banners": true | false, "banner_type": "soft" | "aggressive", "upsell_target_tier": "private_pro_v1", "frequency": "once_per_session" | "every_n_actions" }, "partner_offers": { "enabled": true | false, "show_sponsored_services": true | false, "show_coupons": true | false, "max_offers_per_page": 2 } } } ``` ### 2.3 Példa: "Free" Csomag (`private_free_v1`) ```json { "type": "private", "display_name": "Privát Ingyenes", "pricing": { "monthly_price": 0, "yearly_price": 0, "currency": "EUR", "credit_price": 0 }, "allowances": { "max_vehicles": 1, "max_garages": 1, "monthly_free_credits": 0 }, "entitlements": [], "lifecycle": { "is_public": true }, "ad_policy": { "external_display": { "enabled": true, "networks": ["google_ads"], "max_per_page": 3, "placement": ["sidebar", "between_cards", "footer"] }, "internal_upsell": { "enabled": true, "show_banners": true, "banner_type": "aggressive", "upsell_target_tier": "private_pro_v1", "frequency": "every_n_actions" }, "partner_offers": { "enabled": true, "show_sponsored_services": true, "show_coupons": true, "max_offers_per_page": 2 } } } ``` ### 2.4 Példa: "Premium" Csomag (`corp_premium_v1`) ```json { "type": "corporate", "display_name": "Céges Prémium", "pricing": { "monthly_price": 29.99, "yearly_price": 299.99, "currency": "EUR", "credit_price": 3000 }, "allowances": { "max_vehicles": 20, "max_garages": 3, "monthly_free_credits": 300 }, "entitlements": ["SRV_DATA_EXPORT", "SRV_AI_UPLOAD"], "lifecycle": { "is_public": true }, "ad_policy": { "external_display": { "enabled": false, "networks": [], "max_per_page": 0, "placement": [] }, "internal_upsell": { "enabled": false, "show_banners": false, "banner_type": "soft", "upsell_target_tier": "corp_premium_plus_v1", "frequency": "once_per_session" }, "partner_offers": { "enabled": true, "show_sponsored_services": true, "show_coupons": false, "max_offers_per_page": 1 } } } ``` ### 2.5 Backend Service Terv: `AdPolicyService` Létrehozandó fájl: [`backend/app/services/ad_policy_service.py`](backend/app/services/) ```python class AdPolicyService: """ Szolgáltatás a hirdetési politika lekérdezésére a felhasználó/szervezet aktuális előfizetési csomagja alapján. """ @staticmethod async def get_ad_policy(db: AsyncSession, org_id: int) -> dict: """ Visszaadja a szervezet aktuális ad_policy-ját. Ha nincs tier hozzárendelve, a 'private_free_v1' alapértelmezett policy-t adja. """ pass @staticmethod def should_show_external_ads(policy: dict) -> bool: """Ellenőrzi, hogy a külső hirdetések engedélyezettek-e.""" return policy.get("external_display", {}).get("enabled", False) @staticmethod def should_show_upsell_banner(policy: dict) -> bool: """Ellenőrzi, hogy a belső upsell banner megjelenjen-e.""" return policy.get("internal_upsell", {}).get("enabled", False) ``` ### 2.6 Pydantic Schema Bővítés A [`SubscriptionRulesModel`](backend/app/schemas/subscription.py:55) kiegészítése az `ad_policy` mezővel: ```python class AdPolicyExternalDisplay(BaseModel): enabled: bool = False networks: List[str] = [] max_per_page: int = 0 placement: List[str] = [] class AdPolicyInternalUpsell(BaseModel): enabled: bool = False show_banners: bool = False banner_type: str = "soft" upsell_target_tier: Optional[str] = None frequency: str = "once_per_session" class AdPolicyPartnerOffers(BaseModel): enabled: bool = False show_sponsored_services: bool = False show_coupons: bool = False max_offers_per_page: int = 0 class AdPolicyModel(BaseModel): external_display: AdPolicyExternalDisplay = Field(default_factory=AdPolicyExternalDisplay) internal_upsell: AdPolicyInternalUpsell = Field(default_factory=AdPolicyInternalUpsell) partner_offers: AdPolicyPartnerOffers = Field(default_factory=AdPolicyPartnerOffers) ``` --- ## 3. UI Routing & Catalog — Frontend Terv ### 3.1 Jelenlegi Navigációs Struktúra A frontend két fő layoutot használ: 1. **`PrivateLayout.vue`** ([`frontend/src/layouts/PrivateLayout.vue`](frontend/src/layouts/PrivateLayout.vue)) — Hamburger menü: Dashboard, Járművek, Költségek, Szerviz kereső 2. **`OrganizationLayout.vue`** ([`frontend/src/layouts/OrganizationLayout.vue`](frontend/src/layouts/OrganizationLayout.vue)) — Hamburger menü: Company Data **Hiányzó elem:** Egyik layoutban sincs "Előfizetésem & Csomagok" menüpont. ### 3.2 Tervezett UI Változtatások #### A) Új menüpont: "Előfizetésem & Csomagok" **Helye a `PrivateLayout.vue` hamburger menüben** (a Szerviz kereső után, a lista végén): ```html ``` **Helye az `OrganizationLayout.vue` hamburger menüben** (a Company Data után): ```html ``` #### B) Új Route: `/subscription` A [`router/index.ts`](frontend/src/router/index.ts)-ben új route hozzáadása: ```typescript { path: '/subscription', name: 'subscription', component: () => import('../views/SubscriptionView.vue'), meta: { requiresAuth: true } } ``` #### C) Új View: `SubscriptionView.vue` Létrehozandó: [`frontend/src/views/SubscriptionView.vue`](frontend/src/views/) Funkciók: - Megjeleníti az aktuális csomag adatait (név, lejárati idő, limitek) - Lejárati idő visszaszámláló (countdown timer) - "Csomag váltása" gomb → megnyitja a csomagkatalógust - Hirdetési politika státuszának megjelenítése (pl. "A Te csomagodban nincsenek külső hirdetések") ### 3.3 `GET /api/v1/subscriptions/public` — Publikus Csomagkatalógus API **Tervezett új végpont:** ``` GET /api/v1/subscriptions/public?type=private|corporate ``` **Szűrési logika:** - Ha a garázs `org_type == 'individual'` → csak `private_` prefixű csomagokat ad vissza - Ha a garázs `org_type != 'individual'` (business, fleet_owner, stb.) → csak `corp_` prefixű csomagokat ad vissza - Minden esetben csak azokat a csomagokat, ahol `rules.lifecycle.is_public == true` **Tervezett implementáció:** ```python @router.get("/public", response_model=SubscriptionTierListResponse) async def get_public_subscriptions( org_type: Optional[str] = Query(None, description="Szűrés típus szerint: private vagy corporate"), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Publikus előfizetési csomagok listázása. - Ha org_type='individual', csak private_ csomagok - Ha org_type='business' (vagy más), csak corp_ csomagok - Csak az is_public=true csomagok """ stmt = select(SubscriptionTier) # Szűrés típus szerint if org_type and org_type.lower() == 'individual': stmt = stmt.where(SubscriptionTier.rules["type"].as_string() == "private") elif org_type: stmt = stmt.where(SubscriptionTier.rules["type"].as_string() == "corporate") # Csak publikus csomagok stmt = stmt.where( ~SubscriptionTier.rules["lifecycle"]["is_public"].as_string().in_(["false"]) ) result = await db.execute(stmt.order_by(SubscriptionTier.id)) tiers = result.scalars().all() return SubscriptionTierListResponse( total=len(tiers), tiers=[SubscriptionTierResponse.model_validate(t) for t in tiers] ) ``` --- ## 4. Implementációs Ütemterv ### Fázis 1: Backend Alapok (Elsőbbséges) | # | Feladat | Fájl | Leírás | |---|---------|------|--------| | 1.1 | `valid_until` beállítása | [`billing_engine.py:869`](backend/app/services/billing_engine.py:869) | A `upgrade_org_subscription()` függvényben állítsuk be a `valid_until = now + 30 nap` értéket | | 1.2 | `GET /my` subscription adatokkal | [`organizations.py:180`](backend/app/api/v1/endpoints/organizations.py:180) | Bővítsük ki a választ subscription blokkal (tier adatok + valid_until) | | 1.3 | `AdPolicyModel` Pydantic séma | [`subscription.py`](backend/app/schemas/subscription.py) | Add hozzá az `ad_policy` mezőt a `SubscriptionRulesModel`-hez | | 1.4 | `AdPolicyService` létrehozása | [`services/ad_policy_service.py`](backend/app/services/) | Új service a hirdetési politika lekérdezéséhez | | 1.5 | `GET /api/v1/subscriptions/public` | Új végpont | Publikus csomagkatalógus API org_type alapú szűréssel | ### Fázis 2: Frontend UI | # | Feladat | Fájl | Leírás | |---|---------|------|--------| | 2.1 | "Előfizetésem" menüpont | [`PrivateLayout.vue`](frontend/src/layouts/PrivateLayout.vue) | Új hamburger menüpont | | 2.2 | "Előfizetésem" menüpont | [`OrganizationLayout.vue`](frontend/src/layouts/OrganizationLayout.vue) | Új hamburger menüpont | | 2.3 | `/subscription` route | [`router/index.ts`](frontend/src/router/index.ts) | Új route regisztrálása | | 2.4 | `SubscriptionView.vue` | [`views/SubscriptionView.vue`](frontend/src/views/) | Új nézet: aktuális csomag + countdown + csomagváltás | | 2.5 | i18n kulcsok | [`locales/hu.json`](backend/static/locales/hu.json), [`locales/en.json`](backend/static/locales/en.json) | `menu.subscription` és kapcsolódó fordítások | ### Fázis 3: Seed Adatbázis Frissítés | # | Feladat | Fájl | Leírás | |---|---------|------|--------| | 3.1 | `ad_policy` hozzáadása a seed csomagokhoz | [`seed_packages.py`](backend/app/scripts/seed_packages.py) | Mind a 6 csomag rules kiegészítése az `ad_policy` blokkal | | 3.2 | Seed szkript futtatása | — | `docker compose exec sf_api python3 /app/backend/app/scripts/seed_packages.py` | --- ## 5. Függőségek és Kockázatok | Függőség | Hatás | Megoldás | |----------|-------|----------| | A `valid_until` mező jelenleg NULL minden `org_subscriptions` rekordban | A lejárati idő visszaszámláló nem működik | A `upgrade_org_subscription()` bővítése + backfill szkript a meglévő rekordokhoz | | A frontend auth store (`auth.ts`) jelenleg nem tárol subscription adatokat | A UI nem tudja megjeleníteni a csomag infót | A `GET /my` válasz bővítése után a store automatikusan frissül | | A meglévő seed csomagok nem tartalmaznak `ad_policy`-t | A Smart Ad Policy nem működik a régi csomagokon | A `seed_packages.py` frissítése és újrafuttatása | --- ## 6. Jóváhagyás A fenti terv jóváhagyása után kezdődhet meg a kódolás az alábbi sorrendben: 1. **Backend:** `valid_until` javítás → `GET /my` bővítés → `AdPolicyModel` → `AdPolicyService` → `GET /public` 2. **Seed:** `ad_policy` hozzáadása a csomagokhoz 3. **Frontend:** Menüpontok → Route → SubscriptionView → i18n