admin frontend elkezdése, járművek tisztázása, frontend fejleszts
This commit is contained in:
241
backend/app/scripts/cleanup_user.py
Normal file
241
backend/app/scripts/cleanup_user.py
Normal file
@@ -0,0 +1,241 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Hard Delete Cleanup Script — Cascade törlés User és kapcsolódó rekordokra.
|
||||
|
||||
Használat:
|
||||
docker compose exec sf_api python3 /app/backend/app/scripts/cleanup_user.py --email %test%
|
||||
|
||||
Az --email paraméter részleges egyezést is támogat (SQL LIKE, pl. %test%@%.
|
||||
A szkript tranzakcióban törli:
|
||||
- User rekordot
|
||||
- Person rekordot (ha a User volt az egyetlen kapcsolódó)
|
||||
- Organization rekordot (ha a User volt az egyetlen owner)
|
||||
- Session tokeneket, SocialAccount-okat, Wallet-eket, stb.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from sqlalchemy import select, delete, text
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
# Projekt importok
|
||||
from app.database import async_session_factory
|
||||
from app.models.identity import User, Person, UserRole
|
||||
from app.models.identity.identity import (
|
||||
SocialAccount, Wallet, ActiveVoucher, VerificationToken,
|
||||
UserDeviceLink, UserTrustProfile, OneTimePassword,
|
||||
)
|
||||
from app.models.gamification.gamification import UserStats
|
||||
from app.models.marketplace.service_request import ServiceRequest
|
||||
from app.models.social import ServiceReview
|
||||
from app.models.vehicle.asset import VehicleOwnership
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
logger = logging.getLogger("cleanup_user")
|
||||
|
||||
|
||||
async def cleanup_user(email_pattern: str) -> None:
|
||||
"""
|
||||
Megkeresi a megadott email mintára illeszkedő usereket,
|
||||
és cascade hard delete-et hajt végre rajtuk.
|
||||
"""
|
||||
async with async_session_factory() as db:
|
||||
# 1. Keresés részleges email egyezéssel
|
||||
stmt = (
|
||||
select(User)
|
||||
.where(User.email.ilike(email_pattern))
|
||||
.options(
|
||||
joinedload(User.person),
|
||||
joinedload(User.stats),
|
||||
joinedload(User.wallet),
|
||||
joinedload(User.trust_profile),
|
||||
joinedload(User.ownership_history),
|
||||
joinedload(User.social_accounts),
|
||||
joinedload(User.service_reviews),
|
||||
joinedload(User.service_requests),
|
||||
)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
users = result.unique().scalars().all()
|
||||
|
||||
if not users:
|
||||
logger.warning("❌ Nem található user a '%s' mintára.", email_pattern)
|
||||
return
|
||||
|
||||
logger.info("🔍 Talált userek (%d db):", len(users))
|
||||
for u in users:
|
||||
logger.info(" - ID=%d, Email=%s, Role=%s", u.id, u.email, u.role.value if hasattr(u.role, 'value') else u.role)
|
||||
|
||||
# 2. Tranzakció indítása
|
||||
async with db.begin():
|
||||
for user in users:
|
||||
user_id = user.id
|
||||
email = user.email
|
||||
logger.info("🗑️ Törlés: User ID=%d, Email=%s", user_id, email)
|
||||
|
||||
# --- Cascade törlés ---
|
||||
|
||||
# UserDeviceLink
|
||||
await db.execute(
|
||||
delete(UserDeviceLink).where(UserDeviceLink.user_id == user_id)
|
||||
)
|
||||
logger.info(" ✓ UserDeviceLink törölve")
|
||||
|
||||
# VerificationToken
|
||||
await db.execute(
|
||||
delete(VerificationToken).where(VerificationToken.user_id == user_id)
|
||||
)
|
||||
logger.info(" ✓ VerificationToken törölve")
|
||||
|
||||
# OneTimePassword
|
||||
await db.execute(
|
||||
delete(OneTimePassword).where(OneTimePassword.email == email)
|
||||
)
|
||||
logger.info(" ✓ OneTimePassword törölve")
|
||||
|
||||
# SocialAccount (cascade all, delete-orphan)
|
||||
await db.execute(
|
||||
delete(SocialAccount).where(SocialAccount.user_id == user_id)
|
||||
)
|
||||
logger.info(" ✓ SocialAccount törölve")
|
||||
|
||||
# UserTrustProfile (cascade all, delete-orphan)
|
||||
await db.execute(
|
||||
delete(UserTrustProfile).where(UserTrustProfile.user_id == user_id)
|
||||
)
|
||||
logger.info(" ✓ UserTrustProfile törölve")
|
||||
|
||||
# UserStats (gamification)
|
||||
await db.execute(
|
||||
delete(UserStats).where(UserStats.user_id == user_id)
|
||||
)
|
||||
logger.info(" ✓ UserStats törölve")
|
||||
|
||||
# Wallet + ActiveVoucher (cascade)
|
||||
wallet_stmt = select(Wallet).where(Wallet.user_id == user_id)
|
||||
wallet_result = await db.execute(wallet_stmt)
|
||||
wallet = wallet_result.scalar_one_or_none()
|
||||
if wallet:
|
||||
await db.execute(
|
||||
delete(ActiveVoucher).where(ActiveVoucher.wallet_id == wallet.id)
|
||||
)
|
||||
await db.execute(
|
||||
delete(Wallet).where(Wallet.id == wallet.id)
|
||||
)
|
||||
logger.info(" ✓ Wallet + ActiveVoucher törölve")
|
||||
|
||||
# ServiceReview
|
||||
await db.execute(
|
||||
delete(ServiceReview).where(ServiceReview.user_id == user_id)
|
||||
)
|
||||
logger.info(" ✓ ServiceReview törölve")
|
||||
|
||||
# ServiceRequest
|
||||
await db.execute(
|
||||
delete(ServiceRequest).where(ServiceRequest.user_id == user_id)
|
||||
)
|
||||
logger.info(" ✓ ServiceRequest törölve")
|
||||
|
||||
# VehicleOwnership
|
||||
await db.execute(
|
||||
delete(VehicleOwnership).where(VehicleOwnership.user_id == user_id)
|
||||
)
|
||||
logger.info(" ✓ VehicleOwnership törölve")
|
||||
|
||||
# Person rekord kezelése
|
||||
person = user.person
|
||||
if person:
|
||||
# Ellenőrizzük, hogy más User nem hivatkozik-e erre a Person-ra
|
||||
other_users_stmt = (
|
||||
select(User)
|
||||
.where(User.person_id == person.id, User.id != user_id)
|
||||
.limit(1)
|
||||
)
|
||||
other_result = await db.execute(other_users_stmt)
|
||||
other_user = other_result.scalar_one_or_none()
|
||||
|
||||
if not other_user:
|
||||
# Nincs más User ehhez a Person-hoz -> törölhető
|
||||
await db.execute(
|
||||
delete(Person).where(Person.id == person.id)
|
||||
)
|
||||
logger.info(" ✓ Person ID=%d törölve (egyedüli kapcsolat)", person.id)
|
||||
else:
|
||||
logger.info(
|
||||
" ⚠️ Person ID=%d MEGTARTVA (más User is hivatkozik rá: ID=%d)",
|
||||
person.id, other_user.id
|
||||
)
|
||||
|
||||
# Organization kezelése (ha a User volt az egyetlen owner)
|
||||
from app.models.identity.organization import Organization
|
||||
org_stmt = (
|
||||
select(Organization)
|
||||
.where(Organization.owner_id == user_id)
|
||||
)
|
||||
org_result = await db.execute(org_stmt)
|
||||
owned_orgs = org_result.scalars().all()
|
||||
|
||||
for org in owned_orgs:
|
||||
# Ellenőrizzük, hogy más User nem owner-e
|
||||
other_owner_stmt = (
|
||||
select(User)
|
||||
.where(User.id != user_id, User.id.isnot(None))
|
||||
.limit(1)
|
||||
)
|
||||
# Egyszerűbb: csak töröljük, ha nincs más owner
|
||||
# (a gyakorlatban az Organization-nak lehet több tagja)
|
||||
from app.models.identity.organization import OrganizationMember
|
||||
member_stmt = (
|
||||
select(OrganizationMember)
|
||||
.where(
|
||||
OrganizationMember.organization_id == org.id,
|
||||
OrganizationMember.user_id != user_id,
|
||||
)
|
||||
.limit(1)
|
||||
)
|
||||
member_result = await db.execute(member_stmt)
|
||||
other_member = member_result.scalar_one_or_none()
|
||||
|
||||
if not other_member:
|
||||
await db.execute(
|
||||
delete(Organization).where(Organization.id == org.id)
|
||||
)
|
||||
logger.info(" ✓ Organization ID=%d törölve (egyedüli owner)", org.id)
|
||||
else:
|
||||
logger.info(
|
||||
" ⚠️ Organization ID=%d MEGTARTVA (van más tag)",
|
||||
org.id
|
||||
)
|
||||
|
||||
# VÉGÜL: Maga a User rekord törlése
|
||||
await db.execute(
|
||||
delete(User).where(User.id == user_id)
|
||||
)
|
||||
logger.info(" ✅ User ID=%d (%s) törölve.", user_id, email)
|
||||
|
||||
logger.info("🎉 Tranzakció sikeresen végrehajtva.")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Hard Delete Cleanup — User és kapcsolódó rekordok cascade törlése."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--email",
|
||||
required=True,
|
||||
help="Email minta (SQL LIKE, pl. %%test%% vagy teszt%%).",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
import asyncio
|
||||
asyncio.run(cleanup_user(args.email))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
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())
|
||||
269
backend/app/scripts/seed_packages.py
Normal file
269
backend/app/scripts/seed_packages.py
Normal file
@@ -0,0 +1,269 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Seed szkript a jövőbiztos SaaS csomag-architektúra 1. fázisához.
|
||||
Létrehozza a 6 alap subscription tier-t a system.subscription_tiers táblában.
|
||||
|
||||
Használat:
|
||||
docker compose exec sf_api python3 /app/backend/app/scripts/seed_packages.py
|
||||
|
||||
Architektúra:
|
||||
A csomagok rules JSONB oszlopa egy szabványos struktúrát követ:
|
||||
{
|
||||
"type": "private" | "corporate",
|
||||
"pricing": {"monthly_price": float, "yearly_price": float, "currency": "EUR"},
|
||||
"allowances": {"max_vehicles": int, "max_garages": int, "monthly_free_credits": int},
|
||||
"entitlements": ["SRV_..."],
|
||||
"affiliate": {"commission_rate_percent": int, "referral_bonus_credits": int}
|
||||
}
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from sqlalchemy import select, delete
|
||||
from app.database import AsyncSessionLocal
|
||||
from app.models.core_logic import SubscriptionTier
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||
)
|
||||
logger = logging.getLogger("Seed-Packages")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Csomagdefiníciók
|
||||
# ---------------------------------------------------------------------------
|
||||
PACKAGES = [
|
||||
{
|
||||
"name": "private_free_v1",
|
||||
"rules": {
|
||||
"type": "private",
|
||||
"display_name": "Privát Ingyenes",
|
||||
"pricing": {
|
||||
"monthly_price": 0.0,
|
||||
"yearly_price": 0.0,
|
||||
"currency": "EUR",
|
||||
"credit_price": 0,
|
||||
},
|
||||
"allowances": {
|
||||
"max_vehicles": 1,
|
||||
"max_garages": 1,
|
||||
"monthly_free_credits": 0,
|
||||
},
|
||||
"entitlements": [],
|
||||
"affiliate": {
|
||||
"commission_rate_percent": 0,
|
||||
"referral_bonus_credits": 0,
|
||||
},
|
||||
"lifecycle": {
|
||||
"is_public": True,
|
||||
},
|
||||
},
|
||||
"is_custom": False,
|
||||
},
|
||||
{
|
||||
"name": "private_pro_v1",
|
||||
"rules": {
|
||||
"type": "private",
|
||||
"display_name": "Privát Pro",
|
||||
"pricing": {
|
||||
"monthly_price": 4.99,
|
||||
"yearly_price": 49.99,
|
||||
"currency": "EUR",
|
||||
"credit_price": 500,
|
||||
},
|
||||
"allowances": {
|
||||
"max_vehicles": 3,
|
||||
"max_garages": 1,
|
||||
"monthly_free_credits": 50,
|
||||
},
|
||||
"entitlements": ["SRV_DATA_EXPORT"],
|
||||
"affiliate": {
|
||||
"commission_rate_percent": 10,
|
||||
"referral_bonus_credits": 25,
|
||||
},
|
||||
"lifecycle": {
|
||||
"is_public": True,
|
||||
},
|
||||
},
|
||||
"is_custom": False,
|
||||
},
|
||||
{
|
||||
"name": "private_vip_v1",
|
||||
"rules": {
|
||||
"type": "private",
|
||||
"display_name": "Privát VIP",
|
||||
"pricing": {
|
||||
"monthly_price": 9.99,
|
||||
"yearly_price": 99.99,
|
||||
"currency": "EUR",
|
||||
"credit_price": 1200,
|
||||
},
|
||||
"allowances": {
|
||||
"max_vehicles": 10,
|
||||
"max_garages": 1,
|
||||
"monthly_free_credits": 150,
|
||||
},
|
||||
"entitlements": ["SRV_DATA_EXPORT", "SRV_AI_UPLOAD"],
|
||||
"affiliate": {
|
||||
"commission_rate_percent": 15,
|
||||
"referral_bonus_credits": 50,
|
||||
},
|
||||
"lifecycle": {
|
||||
"is_public": True,
|
||||
},
|
||||
},
|
||||
"is_custom": False,
|
||||
},
|
||||
{
|
||||
"name": "corp_premium_v1",
|
||||
"rules": {
|
||||
"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"],
|
||||
"affiliate": {
|
||||
"commission_rate_percent": 15,
|
||||
"referral_bonus_credits": 100,
|
||||
},
|
||||
"lifecycle": {
|
||||
"is_public": True,
|
||||
},
|
||||
},
|
||||
"is_custom": False,
|
||||
},
|
||||
{
|
||||
"name": "corp_premium_plus_v1",
|
||||
"rules": {
|
||||
"type": "corporate",
|
||||
"display_name": "Céges Prémium Plus",
|
||||
"pricing": {
|
||||
"monthly_price": 59.99,
|
||||
"yearly_price": 599.99,
|
||||
"currency": "EUR",
|
||||
"credit_price": 6000,
|
||||
},
|
||||
"allowances": {
|
||||
"max_vehicles": 50,
|
||||
"max_garages": 10,
|
||||
"monthly_free_credits": 800,
|
||||
},
|
||||
"entitlements": [
|
||||
"SRV_DATA_EXPORT",
|
||||
"SRV_AI_UPLOAD",
|
||||
"SRV_ACCOUNTING_SYNC",
|
||||
],
|
||||
"affiliate": {
|
||||
"commission_rate_percent": 20,
|
||||
"referral_bonus_credits": 250,
|
||||
},
|
||||
"lifecycle": {
|
||||
"is_public": True,
|
||||
},
|
||||
},
|
||||
"is_custom": False,
|
||||
},
|
||||
{
|
||||
"name": "corp_vip_v1",
|
||||
"rules": {
|
||||
"type": "corporate",
|
||||
"display_name": "Céges VIP",
|
||||
"pricing": {
|
||||
"monthly_price": 149.99,
|
||||
"yearly_price": 1499.99,
|
||||
"currency": "EUR",
|
||||
"credit_price": 15000,
|
||||
},
|
||||
"allowances": {
|
||||
"max_vehicles": 200,
|
||||
"max_garages": 50,
|
||||
"monthly_free_credits": 2500,
|
||||
},
|
||||
"entitlements": [
|
||||
"SRV_DATA_EXPORT",
|
||||
"SRV_AI_UPLOAD",
|
||||
"SRV_ACCOUNTING_SYNC",
|
||||
"SRV_API_ACCESS",
|
||||
],
|
||||
"affiliate": {
|
||||
"commission_rate_percent": 25,
|
||||
"referral_bonus_credits": 500,
|
||||
},
|
||||
"lifecycle": {
|
||||
"is_public": True,
|
||||
},
|
||||
},
|
||||
"is_custom": False,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def seed():
|
||||
logger.info("=" * 60)
|
||||
logger.info("Seed Packages - Advanced Subscription Tiers (Phase 1)")
|
||||
logger.info("=" * 60)
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
# 1. Töröljük a meglévő rekordokat (tiszta lap)
|
||||
logger.info("Törlöm a meglévő subscription_tiers rekordokat...")
|
||||
await db.execute(delete(SubscriptionTier))
|
||||
await db.commit()
|
||||
logger.info("✅ Meglévő rekordok törölve.")
|
||||
|
||||
# 2. Beszúrjuk a 6 új csomagot
|
||||
inserted_count = 0
|
||||
for pkg in PACKAGES:
|
||||
tier = SubscriptionTier(
|
||||
name=pkg["name"],
|
||||
rules=pkg["rules"],
|
||||
is_custom=pkg["is_custom"],
|
||||
)
|
||||
db.add(tier)
|
||||
inserted_count += 1
|
||||
logger.info(f" ➕ Hozzáadva: {pkg['name']}")
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"✅ {inserted_count} csomag sikeresen beszúrva.")
|
||||
|
||||
# 3. Visszaigazolás: listázzuk ki az összes rekordot
|
||||
logger.info("-" * 60)
|
||||
logger.info("Visszaigazolás - system.subscription_tiers tartalma:")
|
||||
logger.info("-" * 60)
|
||||
|
||||
result = await db.execute(
|
||||
select(SubscriptionTier).order_by(SubscriptionTier.id)
|
||||
)
|
||||
tiers = result.scalars().all()
|
||||
|
||||
for tier in tiers:
|
||||
rules_json = json.dumps(tier.rules, indent=2, ensure_ascii=False)
|
||||
logger.info(f"\n ID: {tier.id}")
|
||||
logger.info(f" Name: {tier.name}")
|
||||
logger.info(f" Is Custom: {tier.is_custom}")
|
||||
logger.info(f" Rules:\n{rules_json}")
|
||||
logger.info(" " + "-" * 40)
|
||||
|
||||
logger.info(f"\n✅ Összesen {len(tiers)} aktív csomag a rendszerben.")
|
||||
|
||||
# 4. JSONB példa kiírása
|
||||
if tiers:
|
||||
sample = tiers[0]
|
||||
logger.info("=" * 60)
|
||||
logger.info("JSONB PÉLDA (private_free_v1):")
|
||||
logger.info("=" * 60)
|
||||
logger.info(json.dumps(sample.rules, indent=2, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(seed())
|
||||
94
backend/app/scripts/seed_services.py
Normal file
94
backend/app/scripts/seed_services.py
Normal file
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Service Finder - ServiceCatalog Seed Script
|
||||
=============================================
|
||||
Aszinkron seed szkript a system.service_catalog tábla feltöltéséhez.
|
||||
Törli a meglévő adatokat, majd beszúrja a 3 alap szolgáltatást.
|
||||
|
||||
Használat:
|
||||
docker compose exec sf_api python3 /app/backend/app/scripts/seed_services.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Projekt gyökér hozzáadása a Python path-hoz
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||
|
||||
from sqlalchemy import text
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from app.models.core_logic import ServiceCatalog
|
||||
|
||||
|
||||
SERVICES = [
|
||||
{
|
||||
"service_code": "SRV_DATA_EXPORT",
|
||||
"name": "Adat Export",
|
||||
"description": "PDF és Excel adatexport funkciók",
|
||||
"credit_cost": 0,
|
||||
},
|
||||
{
|
||||
"service_code": "SRV_AI_UPLOAD",
|
||||
"name": "AI Számlafeldolgozás",
|
||||
"description": "Dokumentumok és számlák AI alapú beolvasása",
|
||||
"credit_cost": 50,
|
||||
},
|
||||
{
|
||||
"service_code": "SRV_DIGITAL_BOOK",
|
||||
"name": "Digitális Szervizkönyv",
|
||||
"description": "Idegen járművek szerviztörténetének lekérdezése",
|
||||
"credit_cost": 150,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def seed_services():
|
||||
print("=" * 70)
|
||||
print(" ServiceCatalog Seed Script")
|
||||
print("=" * 70)
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
# 1. Tábla tartalmának törlése
|
||||
print("\n🧹 Törlés: system.service_catalog összes rekordja...")
|
||||
await db.execute(text("DELETE FROM system.service_catalog"))
|
||||
await db.commit()
|
||||
print(" ✅ Törölve.")
|
||||
|
||||
# 2. Szolgáltatások beszúrása
|
||||
print("\n📦 Szolgáltatások beszúrása:")
|
||||
for svc in SERVICES:
|
||||
stmt = text("""
|
||||
INSERT INTO system.service_catalog (service_code, name, description, credit_cost, is_active)
|
||||
VALUES (:service_code, :name, :description, :credit_cost, TRUE)
|
||||
""")
|
||||
await db.execute(stmt, svc)
|
||||
print(f" ✅ {svc['service_code']:20s} | {svc['name']:25s} | {svc['credit_cost']:>4} kredit")
|
||||
|
||||
await db.commit()
|
||||
print("\n ✅ Minden rekord beszúrva.")
|
||||
|
||||
# 3. Lekérdezés - bizonyíték
|
||||
print("\n" + "=" * 70)
|
||||
print(" 📋 LEKÉRDEZÉS - system.service_catalog tartalma")
|
||||
print("=" * 70)
|
||||
result = await db.execute(
|
||||
text("SELECT id, service_code, name, description, credit_cost, is_active FROM system.service_catalog ORDER BY id")
|
||||
)
|
||||
rows = result.fetchall()
|
||||
|
||||
if not rows:
|
||||
print("\n❌ NINCSENEK REKORDOK a táblában!")
|
||||
else:
|
||||
print(f"\n{'ID':>4} | {'Kód':20s} | {'Név':25s} | {'Leírás':45s} | {'Ár':>5s} | {'Aktív':>5s}")
|
||||
print("-" * 110)
|
||||
for row in rows:
|
||||
print(f"{row.id:>4} | {row.service_code:20s} | {row.name:25s} | {(row.description or ''):45s} | {row.credit_cost:>5} | {'Igen' if row.is_active else 'Nem'}")
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print(f" Összesen: {len(rows)} rekord a system.service_catalog táblában.")
|
||||
print("=" * 70)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(seed_services())
|
||||
@@ -18,15 +18,7 @@ from app.database import Base
|
||||
from app.core.config import settings
|
||||
|
||||
def dynamic_import_models():
|
||||
models_dir = Path(__file__).parent.parent / "models"
|
||||
for py_file in models_dir.glob("*.py"):
|
||||
if py_file.name == "__init__.py": continue
|
||||
module_name = f"app.models.{py_file.stem}"
|
||||
try:
|
||||
importlib.import_module(module_name)
|
||||
print(f"✅ Imported {module_name}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not import {module_name}: {e}")
|
||||
"""Modellek betöltése a Metadata feltöltéséhez. Egyszerű import az __init__.py-n keresztül."""
|
||||
import app.models
|
||||
print(f"📦 Total tables in Base.metadata: {len(Base.metadata.tables)}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user