szolgáltatók beálltásai, szerkesztése , létrehozása
This commit is contained in:
242
backend/app/scripts/heal_user_data.py
Normal file
242
backend/app/scripts/heal_user_data.py
Normal file
@@ -0,0 +1,242 @@
|
||||
"""
|
||||
🤖 Data Healing Script — Wallet & Referral Code Repair
|
||||
|
||||
Detects and fixes missing Wallet and InvitationCode (referral_code) records
|
||||
for existing User and Organization entities.
|
||||
|
||||
Usage:
|
||||
docker compose exec sf_api python3 /app/backend/app/scripts/heal_user_data.py
|
||||
|
||||
Logs:
|
||||
- Prints a summary of healed records to stdout
|
||||
- Writes detailed log to logs/heal_user_data_{timestamp}.log
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any
|
||||
|
||||
# Ensure the backend app is on the path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
|
||||
from sqlalchemy import select, text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||
|
||||
from app.core.config import settings
|
||||
from app.models.identity import User, Wallet
|
||||
from app.models.marketplace.organization import Organization
|
||||
|
||||
# ── Logging setup ──────────────────────────────────────────────────────────
|
||||
LOG_DIR = "/app/backend/logs"
|
||||
os.makedirs(LOG_DIR, exist_ok=True)
|
||||
|
||||
log_filename = f"heal_user_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
|
||||
log_path = os.path.join(LOG_DIR, log_filename)
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||
handlers=[
|
||||
logging.StreamHandler(sys.stdout),
|
||||
logging.FileHandler(log_path),
|
||||
],
|
||||
)
|
||||
logger = logging.getLogger("heal-user-data")
|
||||
|
||||
|
||||
# ── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
def generate_referral_code() -> str:
|
||||
"""Generate a short, unique referral code (8 chars, uppercase)."""
|
||||
import secrets
|
||||
import string
|
||||
alphabet = string.ascii_uppercase + string.digits
|
||||
return "".join(secrets.choice(alphabet) for _ in range(8))
|
||||
|
||||
|
||||
# ── Main healing logic ─────────────────────────────────────────────────────
|
||||
|
||||
async def heal_users(db: AsyncSession) -> Dict[str, int]:
|
||||
"""
|
||||
Find all Users without a Wallet and create one (0 balance).
|
||||
Find all Users without a referral_code and generate one.
|
||||
"""
|
||||
stats = {"wallet_created": 0, "referral_code_generated": 0}
|
||||
|
||||
# 1. Fetch all users
|
||||
result = await db.execute(select(User))
|
||||
users = result.scalars().all()
|
||||
logger.info(f"Found {len(users)} total User records.")
|
||||
|
||||
for user in users:
|
||||
# ── Wallet check ──
|
||||
wallet_stmt = select(Wallet).where(Wallet.user_id == user.id)
|
||||
wallet_result = await db.execute(wallet_stmt)
|
||||
existing_wallet = wallet_result.scalar_one_or_none()
|
||||
|
||||
if existing_wallet is None:
|
||||
new_wallet = Wallet(
|
||||
user_id=user.id,
|
||||
earned_credits=0,
|
||||
purchased_credits=0,
|
||||
service_coins=0,
|
||||
currency="HUF",
|
||||
)
|
||||
db.add(new_wallet)
|
||||
stats["wallet_created"] += 1
|
||||
logger.info(f" [Wallet] Created for User ID={user.id} ({user.email})")
|
||||
else:
|
||||
logger.debug(f" [Wallet] Already exists for User ID={user.id}")
|
||||
|
||||
# ── Referral code check ──
|
||||
if not user.referral_code:
|
||||
code = generate_referral_code()
|
||||
# Ensure uniqueness
|
||||
while True:
|
||||
dup_check = await db.execute(
|
||||
select(User).where(User.referral_code == code)
|
||||
)
|
||||
if dup_check.scalar_one_or_none() is None:
|
||||
break
|
||||
code = generate_referral_code()
|
||||
|
||||
user.referral_code = code
|
||||
stats["referral_code_generated"] += 1
|
||||
logger.info(
|
||||
f" [Referral] Generated code '{code}' for User ID={user.id} ({user.email})"
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
f" [Referral] Already has code '{user.referral_code}' for User ID={user.id}"
|
||||
)
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
async def heal_organizations(db: AsyncSession) -> Dict[str, int]:
|
||||
"""
|
||||
Check Organizations for wallet coverage.
|
||||
|
||||
NOTE: The `wallets` table has:
|
||||
- user_id: NOT NULL + UNIQUE constraint
|
||||
- organization_id: nullable
|
||||
|
||||
This means each user can have exactly one wallet, and organization
|
||||
wallets share the user's wallet via organization_id. Since user wallets
|
||||
are already created in heal_users(), org wallets are inherently covered.
|
||||
|
||||
We only log existing org wallet links for audit purposes.
|
||||
"""
|
||||
stats = {"org_wallet_created": 0, "org_wallet_skipped": 0}
|
||||
|
||||
result = await db.execute(select(Organization))
|
||||
orgs = result.scalars().all()
|
||||
logger.info(f"Found {len(orgs)} total Organization records.")
|
||||
|
||||
for org in orgs:
|
||||
# Check if an org wallet already exists (linked via organization_id)
|
||||
wallet_stmt = select(Wallet).where(Wallet.organization_id == org.id)
|
||||
wallet_result = await db.execute(wallet_stmt)
|
||||
existing_wallet = wallet_result.scalar_one_or_none()
|
||||
|
||||
if existing_wallet is None:
|
||||
# The wallets.user_id is NOT NULL + UNIQUE, so we cannot create
|
||||
# a separate wallet for an org. The org's owner already has a
|
||||
# personal wallet from heal_users(). We skip org wallet creation
|
||||
# and log the situation.
|
||||
owner_id = getattr(org, 'owner_id', None)
|
||||
if owner_id:
|
||||
# Check if owner has a wallet we could link
|
||||
owner_wallet = await db.execute(
|
||||
select(Wallet).where(Wallet.user_id == owner_id)
|
||||
)
|
||||
owner_wallet = owner_wallet.scalar_one_or_none()
|
||||
if owner_wallet:
|
||||
# Link the existing wallet to this org
|
||||
owner_wallet.organization_id = org.id
|
||||
stats["org_wallet_created"] += 1
|
||||
logger.info(
|
||||
f" [OrgWallet] Linked existing wallet (user_id={owner_id}) "
|
||||
f"to Organization ID={org.id} ({org.name})"
|
||||
)
|
||||
else:
|
||||
stats["org_wallet_skipped"] += 1
|
||||
logger.warning(
|
||||
f" [OrgWallet] SKIPPED for Organization ID={org.id} ({org.name}) — "
|
||||
f"owner (user_id={owner_id}) has no wallet yet."
|
||||
)
|
||||
else:
|
||||
stats["org_wallet_skipped"] += 1
|
||||
logger.warning(
|
||||
f" [OrgWallet] SKIPPED for Organization ID={org.id} ({org.name}) — "
|
||||
f"no owner_id found."
|
||||
)
|
||||
else:
|
||||
logger.debug(f" [OrgWallet] Already linked for Organization ID={org.id}")
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main entry point."""
|
||||
logger.info("=" * 60)
|
||||
logger.info(" DATA HEALING SCRIPT — Wallet & Referral Code Repair")
|
||||
logger.info("=" * 60)
|
||||
|
||||
# Build async engine from the same DATABASE_URL
|
||||
db_url = settings.DATABASE_URL
|
||||
# If the URL starts with postgresql://, convert to postgresql+asyncpg://
|
||||
if db_url.startswith("postgresql://") and "+asyncpg" not in db_url:
|
||||
db_url = db_url.replace("postgresql://", "postgresql+asyncpg://", 1)
|
||||
|
||||
engine = create_async_engine(db_url, echo=False, pool_pre_ping=True)
|
||||
async_session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
async with async_session_factory() as db:
|
||||
try:
|
||||
async with db.begin():
|
||||
user_stats = await heal_users(db)
|
||||
org_stats = await heal_organizations(db)
|
||||
|
||||
total_healed = (
|
||||
user_stats["wallet_created"]
|
||||
+ user_stats["referral_code_generated"]
|
||||
+ org_stats["org_wallet_created"]
|
||||
)
|
||||
|
||||
logger.info("─" * 60)
|
||||
logger.info(" ✅ HEALING COMPLETE — Summary")
|
||||
logger.info(f" User wallets created: {user_stats['wallet_created']}")
|
||||
logger.info(f" Referral codes generated: {user_stats['referral_code_generated']}")
|
||||
logger.info(f" Organization wallets created: {org_stats['org_wallet_created']}")
|
||||
logger.info(f" Organization wallets skipped: {org_stats['org_wallet_skipped']}")
|
||||
logger.info(f" ─────────────────────────────────")
|
||||
logger.info(f" TOTAL records healed: {total_healed}")
|
||||
logger.info("─" * 60)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f" ✅ DATA HEALING COMPLETE")
|
||||
print(f" Log file: {log_path}")
|
||||
print(f"{'='*60}")
|
||||
print(f" User wallets created: {user_stats['wallet_created']}")
|
||||
print(f" Referral codes generated: {user_stats['referral_code_generated']}")
|
||||
print(f" Organization wallets created: {org_stats['org_wallet_created']}")
|
||||
print(f" Organization wallets skipped: {org_stats['org_wallet_skipped']}")
|
||||
print(f" ─────────────────────────────────")
|
||||
print(f" TOTAL records healed: {total_healed}")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Healing failed: {e}", exc_info=True)
|
||||
print(f"\n❌ ERROR: Healing failed — {e}\n")
|
||||
raise
|
||||
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
156
backend/app/scripts/seed_expertise_tags.py
Normal file
156
backend/app/scripts/seed_expertise_tags.py
Normal file
@@ -0,0 +1,156 @@
|
||||
"""
|
||||
Seed script: Feltölti a marketplace.expertise_tags táblát alap kategóriákkal.
|
||||
Idempotens - ON CONFLICT DO NOTHING miatt többször is futtatható.
|
||||
|
||||
Futtatás: docker exec sf_api python3 /app/backend/app/scripts/seed_expertise_tags.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from sqlalchemy import select, func
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from app.models.marketplace.service import ExpertiseTag
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
BASE_CATEGORIES = [
|
||||
{
|
||||
"key": "auto_szerelo",
|
||||
"name_hu": "Autószerelő",
|
||||
"name_en": "Car Mechanic",
|
||||
"category": "vehicle_service",
|
||||
"search_keywords": ["szerelő", "autószerelő", "mechanic", "garage", "javítás"],
|
||||
"description": "Személy- és haszongépjárművek mechanikai javítása, karbantartása",
|
||||
},
|
||||
{
|
||||
"key": "motor_szerelo",
|
||||
"name_hu": "Motorkerékpár szerelő",
|
||||
"name_en": "Motorcycle Mechanic",
|
||||
"category": "vehicle_service",
|
||||
"search_keywords": ["motor", "motorszerelő", "motorcycle", "robogó"],
|
||||
"description": "Motorkerékpárok, robogók javítása és karbantartása",
|
||||
},
|
||||
{
|
||||
"key": "gumiszerviz",
|
||||
"name_hu": "Gumiszerviz",
|
||||
"name_en": "Tire Service",
|
||||
"category": "vehicle_service",
|
||||
"search_keywords": ["gumi", "gumis", "tire", "tyre", "abroncs", "kerék"],
|
||||
"description": "Gumiabroncsok cseréje, javítása, téli/nyári gumik tárolása",
|
||||
},
|
||||
{
|
||||
"key": "karosszerialakatos",
|
||||
"name_hu": "Karosszérialakatos",
|
||||
"name_en": "Body Shop",
|
||||
"category": "body_paint",
|
||||
"search_keywords": ["karosszéria", "lakatos", "bodyshop", "karosszerialakatos", "kasztni"],
|
||||
"description": "Karosszéria javítás, lakatolás, kasztni javítás",
|
||||
},
|
||||
{
|
||||
"key": "fényező",
|
||||
"name_hu": "Fényező",
|
||||
"name_en": "Painter",
|
||||
"category": "body_paint",
|
||||
"search_keywords": ["fényező", "fényezés", "painter", "spray", "lakkozás"],
|
||||
"description": "Gépjármű fényezés, lakkozás, színjavítás",
|
||||
},
|
||||
{
|
||||
"key": "autómentő",
|
||||
"name_hu": "Autómentő / Motormentő",
|
||||
"name_en": "Tow Truck / Roadside Assistance",
|
||||
"category": "roadside",
|
||||
"search_keywords": ["mentő", "autómentő", "tow", "roadside", "assistance", "segély"],
|
||||
"description": "Útsegély, autómentés, motormentés, kiszállás",
|
||||
},
|
||||
{
|
||||
"key": "benzinkút",
|
||||
"name_hu": "Benzinkút",
|
||||
"name_en": "Gas Station",
|
||||
"category": "fuel",
|
||||
"search_keywords": ["benzin", "kút", "gas station", "fuel", "dízel", "töltő"],
|
||||
"description": "Üzemanyag töltőállomások, benzinkutak",
|
||||
},
|
||||
{
|
||||
"key": "alkatrész_kereskedés",
|
||||
"name_hu": "Alkatrész kereskedés",
|
||||
"name_en": "Parts Store",
|
||||
"category": "parts",
|
||||
"search_keywords": ["alkatrész", "parts", "spares", "autóalkatrész"],
|
||||
"description": "Gépjármű alkatrészek kereskedelme",
|
||||
},
|
||||
{
|
||||
"key": "vizsgaállomás",
|
||||
"name_hu": "Vizsgaállomás",
|
||||
"name_en": "Inspection Station",
|
||||
"category": "inspection",
|
||||
"search_keywords": ["vizsga", "műszaki", "inspection", "MOT", "vizsgáztatás"],
|
||||
"description": "Műszaki vizsgáztatás, emisszió mérés",
|
||||
},
|
||||
{
|
||||
"key": "autókozmetika",
|
||||
"name_hu": "Autókozmetika",
|
||||
"name_en": "Car Detailing",
|
||||
"category": "detailing",
|
||||
"search_keywords": ["kozmetika", "detailing", "takarítás", "mosás", "polírozás"],
|
||||
"description": "Autókozmetika, részletes takarítás, polírozás, kerámia bevonat",
|
||||
},
|
||||
{
|
||||
"key": "egyéb",
|
||||
"name_hu": "Egyéb szolgáltató",
|
||||
"name_en": "Other Service",
|
||||
"category": "other",
|
||||
"search_keywords": ["egyéb", "other", "szolgáltató", "service"],
|
||||
"description": "Egyéb gépjárművel kapcsolatos szolgáltatás",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def seed_expertise_tags():
|
||||
"""Feltölti az expertise_tags táblát az alap kategóriákkal."""
|
||||
async with AsyncSessionLocal() as db:
|
||||
try:
|
||||
# Ellenőrizzük, van-e már adat
|
||||
stmt = select(func.count(ExpertiseTag.id))
|
||||
result = await db.execute(stmt)
|
||||
count = result.scalar()
|
||||
|
||||
if count > 0:
|
||||
logger.info(f"Az expertise_tags tábla már tartalmaz {count} rekordot. Feltöltés kihagyva.")
|
||||
print(f"✅ Az expertise_tags tábla már tartalmaz {count} rekordot. Nincs szükség seed-elésre.")
|
||||
return
|
||||
|
||||
inserted = 0
|
||||
for cat in BASE_CATEGORIES:
|
||||
tag = ExpertiseTag(
|
||||
key=cat["key"],
|
||||
name_hu=cat["name_hu"],
|
||||
name_en=cat["name_en"],
|
||||
category=cat["category"],
|
||||
search_keywords=cat["search_keywords"],
|
||||
description=cat["description"],
|
||||
is_official=True,
|
||||
discovery_points=10,
|
||||
usage_count=0,
|
||||
)
|
||||
db.add(tag)
|
||||
inserted += 1
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"Sikeresen beszúrva {inserted} kategória az expertise_tags táblába.")
|
||||
print(f"✅ Sikeresen beszúrva {inserted} kategória az expertise_tags táblába.")
|
||||
|
||||
# Ellenőrzés
|
||||
result = await db.execute(select(func.count(ExpertiseTag.id)))
|
||||
final_count = result.scalar()
|
||||
print(f"📊 Összes rekord az expertise_tags táblában: {final_count}")
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"Hiba a seed-elés során: {e}")
|
||||
print(f"❌ Hiba: {e}")
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
asyncio.run(seed_expertise_tags())
|
||||
118
backend/app/scripts/seed_gamification_rules.py
Normal file
118
backend/app/scripts/seed_gamification_rules.py
Normal file
@@ -0,0 +1,118 @@
|
||||
"""
|
||||
Gamification Point Rules Seed Script
|
||||
=====================================
|
||||
Cél: Dinamikus pontszabályok feltöltése a gamification.point_rules táblába.
|
||||
A pontértékek NINCSENEK beégetve a kódba, hanem az adatbázisból
|
||||
(point_rules tábla) olvassuk ki őket futásidőben.
|
||||
|
||||
A Vezető Tervező utasítása szerint:
|
||||
- TILOS hardcoded pontszámokat használni a service rétegben!
|
||||
- Minden pontértéket a point_rules táblából kell lekérdezni action_key alapján.
|
||||
|
||||
Használat:
|
||||
docker compose exec sf_api python3 /app/backend/app/scripts/seed_gamification_rules.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Projekt gyökér hozzáadása a Python path-hoz
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||
from app.core.config import settings
|
||||
from app.models.gamification.gamification import PointRule
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
logger = logging.getLogger("Seed-Gamification-Rules")
|
||||
|
||||
# A seed-elendő pontszabályok definíciói
|
||||
# FIGYELEM: Ezek a definíciók csak a seed script számára vannak itt.
|
||||
# A service rétegben a pontokat az adatbázisból (point_rules tábla) kell kiolvasni!
|
||||
SEED_RULES = [
|
||||
{
|
||||
"action_key": "ADD_NEW_PROVIDER",
|
||||
"points": 500,
|
||||
"description": "Új szolgáltató rögzítése a rendszerbe",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"action_key": "USE_UNVERIFIED_PROVIDER",
|
||||
"points": 200,
|
||||
"description": "Szervizesemény/költség rögzítése olyan szolgáltatónál, aminek még nincs 5 megerősítése",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"action_key": "RATE_PROVIDER",
|
||||
"points": 250,
|
||||
"description": "Szolgáltató értékelése (tagekkel)",
|
||||
"is_active": True,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def seed_point_rules():
|
||||
"""
|
||||
Feltölti a gamification.point_rules táblát a fenti szabályokkal,
|
||||
de csak azokat a rekordokat szúrja be, amelyek még nem léteznek
|
||||
(action_key alapján UPSERT helyett INSERT WHERE NOT EXISTS).
|
||||
"""
|
||||
# Adatbázis kapcsolat létrehozása
|
||||
engine = create_async_engine(settings.DATABASE_URL, echo=False)
|
||||
async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
async with async_session() as db:
|
||||
try:
|
||||
inserted_count = 0
|
||||
skipped_count = 0
|
||||
|
||||
for rule_data in SEED_RULES:
|
||||
# Ellenőrizzük, hogy létezik-e már ez a szabály
|
||||
stmt = select(PointRule).where(
|
||||
PointRule.action_key == rule_data["action_key"]
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
existing = result.scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
logger.info(
|
||||
f"⏭️ Szabály már létezik: {rule_data['action_key']} "
|
||||
f"(pont: {existing.points}, ID: {existing.id})"
|
||||
)
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
# Új szabály beszúrása
|
||||
new_rule = PointRule(
|
||||
action_key=rule_data["action_key"],
|
||||
points=rule_data["points"],
|
||||
description=rule_data["description"],
|
||||
is_active=rule_data["is_active"],
|
||||
)
|
||||
db.add(new_rule)
|
||||
await db.flush()
|
||||
logger.info(
|
||||
f"✅ Szabály létrehozva: {rule_data['action_key']} "
|
||||
f"(pont: {rule_data['points']}, ID: {new_rule.id})"
|
||||
)
|
||||
inserted_count += 1
|
||||
|
||||
await db.commit()
|
||||
logger.info(
|
||||
f"\n📊 Összegzés: {inserted_count} új szabály beszúrva, "
|
||||
f"{skipped_count} már létezett."
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"❌ Hiba a seedelés során: {e}", exc_info=True)
|
||||
raise
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(seed_point_rules())
|
||||
Reference in New Issue
Block a user