admin frontend elkezdése, járművek tisztázása, frontend fejleszts

This commit is contained in:
Roo
2026-06-15 18:52:38 +00:00
parent ef8df9608c
commit 213ba3b0f1
80 changed files with 11615 additions and 2304 deletions

View File

@@ -0,0 +1,363 @@
#!/usr/bin/env python3
"""
📦 Nagy Előfizetés Migráció (Subscription Migration Script)
Cél:
A régi, denormalizált subscription_plan mezők (identity.users, fleet.organizations)
átvezetése a modern, normalizált subscription_tier + org_subscriptions / user_subscriptions
architektúrába.
Feladatok:
A) Létrehozza a corp_free_v1 csomagot, ha még nem létezik (0 áras, alap korlátokkal).
B) Létrehozza a user_subscriptions táblát (finance.user_subscriptions), ha még nem létezik.
C) Adatkonverzió: a meglévő subscription_plan string-eket átalakítja tier_id hivatkozásokká.
Használat:
docker compose exec sf_api python3 /app/backend/app/scripts/migrate_subscriptions.py
Architektúra:
- system.subscription_tiers: A csomagdefiníciók (SubscriptionTier modell)
- finance.org_subscriptions: Szervezeti előfizetések (OrganizationSubscription modell)
- finance.user_subscriptions: Felhasználói előfizetések (UserSubscription modell - ÚJ!)
"""
import asyncio
import logging
from datetime import datetime, timezone
from sqlalchemy import text, select, Integer, String, Boolean, DateTime, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.sql import func
from app.database import AsyncSessionLocal, Base
from app.models.core_logic import SubscriptionTier, OrganizationSubscription
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
)
logger = logging.getLogger("Migrate-Subscriptions")
# ---------------------------------------------------------------------------
# Tiers név -> ID mapping (feltöltés után)
# ---------------------------------------------------------------------------
TIER_MAP: dict[str, int] = {}
# ---------------------------------------------------------------------------
# A) corp_free_v1 definíció
# ---------------------------------------------------------------------------
CORP_FREE_V1 = {
"name": "corp_free_v1",
"rules": {
"type": "corporate",
"display_name": "Céges Ingyenes",
"pricing": {
"monthly_price": 0.0,
"yearly_price": 0.0,
"currency": "EUR",
"credit_price": 0,
},
"allowances": {
"max_vehicles": 3,
"max_garages": 1,
"monthly_free_credits": 0,
},
"entitlements": [],
"affiliate": {
"commission_rate_percent": 0,
"referral_bonus_credits": 0,
},
"lifecycle": {
"is_public": True,
},
},
"is_custom": False,
}
# ---------------------------------------------------------------------------
# Régi plan string -> tier név mapping
# ---------------------------------------------------------------------------
PLAN_TO_TIER: dict[str, str] = {
"FREE": "private_free_v1",
"free": "private_free_v1",
"PRO": "private_pro_v1",
"PREMIUM": "corp_premium_v1",
"ENTERPRISE": "corp_vip_v1",
}
# ---------------------------------------------------------------------------
# Segédfüggvények
# ---------------------------------------------------------------------------
async def _ensure_corp_free_v1(db) -> None:
"""A) Létrehozza a corp_free_v1 csomagot, ha még nem létezik."""
result = await db.execute(
select(SubscriptionTier).where(SubscriptionTier.name == "corp_free_v1")
)
existing = result.scalar_one_or_none()
if existing:
logger.info("✅ corp_free_v1 már létezik (ID=%d). Kihagyva.", existing.id)
TIER_MAP["corp_free_v1"] = existing.id
return
tier = SubscriptionTier(
name=CORP_FREE_V1["name"],
rules=CORP_FREE_V1["rules"],
is_custom=CORP_FREE_V1["is_custom"],
)
db.add(tier)
await db.commit()
await db.refresh(tier)
TIER_MAP["corp_free_v1"] = tier.id
logger.info("✅ corp_free_v1 létrehozva (ID=%d).", tier.id)
async def _load_tier_map(db) -> None:
"""Betölti az összes tier nevét és ID-ját a TIER_MAP-ba."""
result = await db.execute(select(SubscriptionTier))
tiers = result.scalars().all()
for t in tiers:
TIER_MAP[t.name] = t.id
logger.info("📋 Betöltött tier-ek: %s", {n: i for n, i in TIER_MAP.items()})
def _resolve_tier_id(plan: str | None) -> int | None:
"""Egy régi plan string-ből kikeresi a megfelelő tier ID-t."""
if not plan:
return None
tier_name = PLAN_TO_TIER.get(plan)
if not tier_name:
logger.warning("⚠️ Ismeretlen plan: '%s' -> private_free_v1 lesz.", plan)
tier_name = "private_free_v1"
return TIER_MAP.get(tier_name)
# ---------------------------------------------------------------------------
# B) UserSubscription tábla létrehozása (ha nem létezik)
# ---------------------------------------------------------------------------
async def _ensure_user_subscriptions_table(db) -> None:
"""Létrehozza a finance.user_subscriptions táblát, ha még nem létezik."""
result = await db.execute(
text(
"SELECT EXISTS (SELECT FROM information_schema.tables "
"WHERE table_schema = 'finance' AND table_name = 'user_subscriptions')"
)
)
exists = result.scalar()
if exists:
logger.info("✅ finance.user_subscriptions tábla már létezik.")
return
logger.info("🛠️ Létrehozom a finance.user_subscriptions táblát...")
await db.execute(
text("""
CREATE TABLE finance.user_subscriptions (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES identity.users(id) ON DELETE CASCADE,
tier_id INTEGER NOT NULL REFERENCES system.subscription_tiers(id),
valid_from TIMESTAMPTZ NOT NULL DEFAULT NOW(),
valid_until TIMESTAMPTZ,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ
)
""")
)
# Index a gyors lekérdezéshez
await db.execute(
text("""
CREATE INDEX idx_user_subscriptions_user_id
ON finance.user_subscriptions(user_id)
""")
)
await db.commit()
logger.info("✅ finance.user_subscriptions tábla létrehozva.")
# ---------------------------------------------------------------------------
# C) Adatkonverzió
# ---------------------------------------------------------------------------
async def _migrate_organizations(db) -> None:
"""Szervezetek subscription_plan mezőjének átvezetése org_subscriptions-ba."""
logger.info("=" * 60)
logger.info("Szervezetek migrálása...")
logger.info("=" * 60)
# 1. Lekérdezzük az összes szervezetet, aminek nincs még org_subscription-e
result = await db.execute(
text("""
SELECT o.id, o.subscription_plan
FROM fleet.organizations o
WHERE o.is_deleted = FALSE
AND NOT EXISTS (
SELECT 1 FROM finance.org_subscriptions os
WHERE os.org_id = o.id
)
ORDER BY o.id
""")
)
orgs = result.all()
logger.info("📊 %d szervezet vár migrálásra.", len(orgs))
inserted = 0
skipped = 0
for org in orgs:
tier_id = _resolve_tier_id(org.subscription_plan)
if tier_id is None:
logger.warning("⚠️ Org ID=%d: plan='%s' nem feloldható, kihagyva.", org.id, org.subscription_plan)
skipped += 1
continue
sub = OrganizationSubscription(
org_id=org.id,
tier_id=tier_id,
valid_from=datetime.now(timezone.utc),
valid_until=None,
is_active=True,
)
db.add(sub)
inserted += 1
await db.commit()
logger.info("%d org_subscription beszúrva. (%d kihagyva)", inserted, skipped)
async def _migrate_users(db) -> None:
"""Felhasználók subscription_plan mezőjének átvezetése user_subscriptions-ba."""
logger.info("=" * 60)
logger.info("Felhasználók migrálása...")
logger.info("=" * 60)
# 1. Lekérdezzük az összes aktív felhasználót, aminek nincs még user_subscription-e
result = await db.execute(
text("""
SELECT u.id, u.subscription_plan
FROM identity.users u
WHERE u.is_deleted = FALSE
AND NOT EXISTS (
SELECT 1 FROM finance.user_subscriptions us
WHERE us.user_id = u.id
)
ORDER BY u.id
""")
)
users = result.all()
logger.info("📊 %d felhasználó vár migrálásra.", len(users))
inserted = 0
skipped = 0
for user in users:
tier_id = _resolve_tier_id(user.subscription_plan)
if tier_id is None:
logger.warning("⚠️ User ID=%d: plan='%s' nem feloldható, kihagyva.", user.id, user.subscription_plan)
skipped += 1
continue
await db.execute(
text("""
INSERT INTO finance.user_subscriptions
(user_id, tier_id, valid_from, is_active, created_at)
VALUES
(:uid, :tid, NOW(), TRUE, NOW())
"""),
{"uid": user.id, "tid": tier_id},
)
inserted += 1
await db.commit()
logger.info("%d user_subscription beszúrva. (%d kihagyva)", inserted, skipped)
async def _verify_migration(db) -> None:
"""Ellenőrzi a migráció sikerességét."""
logger.info("=" * 60)
logger.info("🔍 Ellenőrzés")
logger.info("=" * 60)
# Tiers
result = await db.execute(
text("SELECT id, name FROM system.subscription_tiers ORDER BY id")
)
tiers = result.all()
logger.info("📋 Tiers (%d db):", len(tiers))
for t in tiers:
logger.info(" ID=%d name=%s", t.id, t.name)
# Org subscriptions
result = await db.execute(
text("""
SELECT COUNT(*) as cnt,
COALESCE(t.name, 'N/A') as tier_name
FROM finance.org_subscriptions os
LEFT JOIN system.subscription_tiers t ON t.id = os.tier_id
GROUP BY t.name
ORDER BY t.name
""")
)
rows = result.all()
total_org = sum(r.cnt for r in rows)
logger.info("📊 Org subscriptions (%d db):", total_org)
for r in rows:
logger.info(" %s: %d", r.tier_name, r.cnt)
# User subscriptions
result = await db.execute(
text("""
SELECT COUNT(*) as cnt,
COALESCE(t.name, 'N/A') as tier_name
FROM finance.user_subscriptions us
LEFT JOIN system.subscription_tiers t ON t.id = us.tier_id
GROUP BY t.name
ORDER BY t.name
""")
)
rows = result.all()
total_user = sum(r.cnt for r in rows)
logger.info("📊 User subscriptions (%d db):", total_user)
for r in rows:
logger.info(" %s: %d", r.tier_name, r.cnt)
logger.info("=" * 60)
logger.info("✅ Migráció befejeződött!")
logger.info(" Összes org subscription: %d", total_org)
logger.info(" Összes user subscription: %d", total_user)
logger.info("=" * 60)
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
async def migrate():
logger.info("=" * 60)
logger.info("📦 Nagy Előfizetés Migráció")
logger.info("=" * 60)
async with AsyncSessionLocal() as db:
# A) corp_free_v1 létrehozása
logger.info("\n[1/5] corp_free_v1 csomag ellenőrzése...")
await _ensure_corp_free_v1(db)
# Tier map betöltése
logger.info("\n[2/5] Tier-ek betöltése...")
await _load_tier_map(db)
# B) user_subscriptions tábla létrehozása
logger.info("\n[3/5] user_subscriptions tábla ellenőrzése...")
await _ensure_user_subscriptions_table(db)
# C) Adatkonverzió
logger.info("\n[4/5] Adatkonverzió...")
await _migrate_organizations(db)
await _migrate_users(db)
# Ellenőrzés
logger.info("\n[5/5] Ellenőrzés...")
await _verify_migration(db)
if __name__ == "__main__":
asyncio.run(migrate())