RABAC felépítése megtörtént ill hirdetési portál alapok lerakva
This commit is contained in:
428
plans/p0_subscription_ui_timers_ad_policy_plan.md
Normal file
428
plans/p0_subscription_ui_timers_ad_policy_plan.md
Normal 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
|
||||
Reference in New Issue
Block a user