16 KiB
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 - Kulcs mezők:
id,name(unique),rules(JSONB),is_custom - JSONB
rulesstruktúra: Lásd aseed_packages.pydefinícióit - Jelenlegi csomagok: 10 db (4 private + 6 corporate)
B) finance.org_subscriptions — Szervezeti előfizetés rekordok
- Modell:
OrganizationSubscription - Kulcs mezők:
org_id→ FKfleet.organizations.idtier_id→ FKsystem.subscription_tiers.idvalid_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 - 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 - 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() 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 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 végpont jelenleg nem adja vissza az előfizetési adatokat. Tervezett bővítés:
# 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:
- A
GET /myvégpontban minden Organization rekordhoz töltsük be a hozzá tartozó aktívOrganizationSubscriptionrekordot (JOIN vagy selectinload). - Ha van aktív subscription, adjuk vissza a
valid_until,tier.rules(allowances, pricing, display_name) adatokat. - 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
{
"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)
{
"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)
{
"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
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 kiegészítése az ad_policy mezővel:
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:
PrivateLayout.vue(frontend/src/layouts/PrivateLayout.vue) — Hamburger menü: Dashboard, Járművek, Költségek, Szerviz kereső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):
<!-- 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):
<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-ben új route hozzáadása:
{
path: '/subscription',
name: 'subscription',
component: () => import('../views/SubscriptionView.vue'),
meta: { requiresAuth: true }
}
C) Új View: SubscriptionView.vue
Létrehozandó: frontend/src/views/SubscriptionView.vue
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'→ csakprivate_prefixű csomagokat ad vissza - Ha a garázs
org_type != 'individual'(business, fleet_owner, stb.) → csakcorp_prefixű csomagokat ad vissza - Minden esetben csak azokat a csomagokat, ahol
rules.lifecycle.is_public == true
Tervezett implementáció:
@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 |
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 |
Bővítsük ki a választ subscription blokkal (tier adatok + valid_until) |
| 1.3 | AdPolicyModel Pydantic séma |
subscription.py |
Add hozzá az ad_policy mezőt a SubscriptionRulesModel-hez |
| 1.4 | AdPolicyService létrehozása |
services/ad_policy_service.py |
Ú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 |
Új hamburger menüpont |
| 2.2 | "Előfizetésem" menüpont | OrganizationLayout.vue |
Új hamburger menüpont |
| 2.3 | /subscription route |
router/index.ts |
Új route regisztrálása |
| 2.4 | SubscriptionView.vue |
views/SubscriptionView.vue |
Új nézet: aktuális csomag + countdown + csomagváltás |
| 2.5 | i18n kulcsok | locales/hu.json, 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 |
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:
- Backend:
valid_untiljavítás →GET /mybővítés →AdPolicyModel→AdPolicyService→GET /public - Seed:
ad_policyhozzáadása a csomagokhoz - Frontend: Menüpontok → Route → SubscriptionView → i18n