RABAC felépítése megtörtént ill hirdetési portál alapok lerakva

This commit is contained in:
Roo
2026-06-19 06:06:34 +00:00
parent fe3c32597d
commit 9ba2d9180d
30 changed files with 4336 additions and 76 deletions

View File

@@ -0,0 +1,428 @@
# 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
<!-- Előfizetésem & Csomagok -->
<button
@click="navigateToSubscription"
class="flex w-full items-center gap-3 px-4 py-2.5 text-sm text-white/80 transition-all duration-150 hover:bg-white/5 hover:text-white"
>
<svg class="w-4 h-4 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
</svg>
{{ t('menu.subscription') }}
</button>
```
**Helye az `OrganizationLayout.vue` hamburger menüben** (a Company Data után):
```html
<button
@click="navigateToSubscription"
class="flex w-full items-center gap-3 px-4 py-2.5 text-sm text-white/80 transition-all duration-150 hover:bg-white/5 hover:text-white"
>
<!-- Wallet/Subscription icon -->
<svg class="w-4 h-4 text-white/40" ...>
{{ t('menu.subscription') }}
</button>
```
#### 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