admin frontend elkezdése, járművek tisztázása, frontend fejleszts
This commit is contained in:
363
backend/app/scripts/migrate_subscriptions.py
Normal file
363
backend/app/scripts/migrate_subscriptions.py
Normal 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())
|
||||
Reference in New Issue
Block a user