frontend admin refakctorálás
This commit is contained in:
131
backend/app/scripts/migrate_i18n_jsonb.py
Normal file
131
backend/app/scripts/migrate_i18n_jsonb.py
Normal file
@@ -0,0 +1,131 @@
|
||||
"""
|
||||
i18n JSONB Migration Script (Phase 1)
|
||||
Migrates old name_hu/name_en columns to name_i18n JSONB format.
|
||||
Run INSIDE the sf_api container:
|
||||
docker exec sf_api python3 /app/backend/app/scripts/migrate_i18n_jsonb.py
|
||||
|
||||
IMPORTANT: Run this AFTER the new JSONB columns have been added by sync_engine.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||
|
||||
from sqlalchemy import text
|
||||
from app.db.session import AsyncSessionLocal
|
||||
|
||||
TABLES = [
|
||||
{
|
||||
"schema": "marketplace",
|
||||
"table": "expertise_tags",
|
||||
"name_cols": ["name_hu", "name_en"],
|
||||
"name_translations_col": "name_translations",
|
||||
"desc_cols": ["description"],
|
||||
"target_name_col": "name_i18n",
|
||||
"target_desc_col": "description_i18n",
|
||||
},
|
||||
{
|
||||
"schema": "vehicle",
|
||||
"table": "dict_body_types",
|
||||
"name_cols": ["name_hu"],
|
||||
"name_translations_col": None,
|
||||
"desc_cols": [],
|
||||
"target_name_col": "name_i18n",
|
||||
"target_desc_col": None,
|
||||
},
|
||||
{
|
||||
"schema": "system",
|
||||
"table": "service_catalog",
|
||||
"name_cols": ["name"],
|
||||
"name_translations_col": None,
|
||||
"desc_cols": ["description"],
|
||||
"target_name_col": "name_i18n",
|
||||
"target_desc_col": "description_i18n",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def migrate_table(config: dict):
|
||||
schema = config["schema"]
|
||||
table = config["table"]
|
||||
target_name = config["target_name_col"]
|
||||
target_desc = config["target_desc_col"]
|
||||
name_cols = config["name_cols"]
|
||||
desc_cols = config["desc_cols"]
|
||||
trans_col = config["name_translations_col"]
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f" Migrating: {schema}.{table}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
select_cols = ["id"] + name_cols + desc_cols
|
||||
if trans_col:
|
||||
select_cols.append(trans_col)
|
||||
|
||||
stmt = text(
|
||||
f"SELECT {', '.join(select_cols)} FROM {schema}.{table} "
|
||||
f"WHERE {target_name} IS NULL OR {target_name} = '{{\"hu\": \"\"}}'::jsonb"
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
rows = result.fetchall()
|
||||
|
||||
if not rows:
|
||||
print(f" ✅ No rows need migration in {schema}.{table}")
|
||||
return
|
||||
|
||||
updated = 0
|
||||
for row in rows:
|
||||
row_dict = row._mapping
|
||||
|
||||
# Build name_i18n
|
||||
name_i18n = {}
|
||||
if len(name_cols) >= 1 and row_dict.get(name_cols[0]):
|
||||
name_i18n["hu"] = row_dict[name_cols[0]]
|
||||
if len(name_cols) >= 2 and row_dict.get(name_cols[1]):
|
||||
name_i18n["en"] = row_dict[name_cols[1]]
|
||||
|
||||
# Merge with existing name_translations
|
||||
if trans_col and row_dict.get(trans_col):
|
||||
if isinstance(row_dict[trans_col], dict):
|
||||
name_i18n.update(row_dict[trans_col])
|
||||
|
||||
# Build description_i18n
|
||||
desc_i18n = None
|
||||
if desc_cols and row_dict.get(desc_cols[0]):
|
||||
desc_i18n = {"hu": row_dict[desc_cols[0]]}
|
||||
|
||||
# Update
|
||||
update_cols = [f"{target_name} = :name_val"]
|
||||
params = {"name_val": json.dumps(name_i18n), "row_id": row_dict["id"]}
|
||||
|
||||
if target_desc and desc_i18n:
|
||||
update_cols.append(f"{target_desc} = :desc_val")
|
||||
params["desc_val"] = json.dumps(desc_i18n)
|
||||
|
||||
update_sql = f"UPDATE {schema}.{table} SET {', '.join(update_cols)} WHERE id = :row_id"
|
||||
await db.execute(text(update_sql), params)
|
||||
updated += 1
|
||||
|
||||
await db.commit()
|
||||
print(f" ✅ Migrated {updated} rows in {schema}.{table}")
|
||||
|
||||
|
||||
async def main():
|
||||
print("=" * 70)
|
||||
print(" i18n JSONB Migration Script (Phase 1)")
|
||||
print("=" * 70)
|
||||
|
||||
for table_config in TABLES:
|
||||
await migrate_table(table_config)
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print(" ✅ Migration complete!")
|
||||
print("=" * 70)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
259
backend/app/scripts/migrate_provider_addresses.py
Normal file
259
backend/app/scripts/migrate_provider_addresses.py
Normal file
@@ -0,0 +1,259 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
P0 ADDRESS UNIFICATION - Phase 1: ServiceProvider Address Migration
|
||||
|
||||
Migrates flat address columns (city, address_zip, address_street_name, etc.)
|
||||
from marketplace.service_providers into the centralized system.addresses table.
|
||||
|
||||
Logic:
|
||||
1. Fetch all ServiceProvider records where address_id IS NULL
|
||||
but at least one flat address field is non-empty.
|
||||
2. For each record, construct an AddressIn schema from the flat data.
|
||||
3. Call AddressManager.create_or_update(db, None, address_data) to create
|
||||
a new central address record.
|
||||
4. Assign the returned Address UUID to provider.address_id.
|
||||
5. Commit in batches with proper error handling and logging.
|
||||
|
||||
Usage:
|
||||
docker exec sf_api python3 /app/backend/app/scripts/migrate_provider_addresses.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
# Ensure the backend package is importable
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
# asyncpg needs plain "postgresql://" scheme
|
||||
_raw_url = os.getenv(
|
||||
"DATABASE_URL",
|
||||
"postgresql://service_finder_app:AppSafePass_2026@db:5432/service_finder"
|
||||
)
|
||||
DATABASE_URL = _raw_url.replace("+asyncpg", "")
|
||||
|
||||
# Batch size for commits
|
||||
BATCH_SIZE = 100
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||
handlers=[logging.StreamHandler(sys.stdout)],
|
||||
)
|
||||
logger = logging.getLogger("migrate_provider_addresses")
|
||||
|
||||
|
||||
def _build_full_address_text(row: dict) -> Optional[str]:
|
||||
"""Build a human-readable full address from flat fields."""
|
||||
parts = []
|
||||
if row.get("address_zip"):
|
||||
parts.append(row["address_zip"])
|
||||
if row.get("city"):
|
||||
parts.append(row["city"])
|
||||
street_parts = []
|
||||
if row.get("address_street_name"):
|
||||
street_parts.append(row["address_street_name"])
|
||||
if row.get("address_street_type"):
|
||||
street_parts.append(row["address_street_type"])
|
||||
if row.get("address_house_number"):
|
||||
street_parts.append(row["address_house_number"])
|
||||
if street_parts:
|
||||
parts.append(" ".join(street_parts))
|
||||
return ", ".join(parts) if parts else None
|
||||
|
||||
|
||||
def _has_address_data(row: dict) -> bool:
|
||||
"""Check if the provider has any flat address data worth migrating."""
|
||||
return bool(
|
||||
row.get("city")
|
||||
or row.get("address_zip")
|
||||
or row.get("address_street_name")
|
||||
or row.get("address_house_number")
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
logger.info("=" * 70)
|
||||
logger.info("🔧 P0 ADDRESS UNIFICATION - Phase 1: ServiceProvider Migration")
|
||||
logger.info("=" * 70)
|
||||
logger.info(f"Started at: {datetime.now(timezone.utc).isoformat()}")
|
||||
logger.info(f"Database URL: {DATABASE_URL.replace('AppSafePass_2026', '****')}")
|
||||
logger.info(f"Batch size: {BATCH_SIZE}")
|
||||
logger.info("")
|
||||
|
||||
import asyncpg
|
||||
|
||||
conn = await asyncpg.connect(DATABASE_URL)
|
||||
|
||||
try:
|
||||
# ── Step 1: Count providers needing migration ──
|
||||
logger.info("📋 Step 1: Counting providers with flat address data but no address_id")
|
||||
count_row = await conn.fetchrow("""
|
||||
SELECT COUNT(*) AS cnt
|
||||
FROM marketplace.service_providers
|
||||
WHERE address_id IS NULL
|
||||
AND (
|
||||
city IS NOT NULL AND city != ''
|
||||
OR address_zip IS NOT NULL AND address_zip != ''
|
||||
OR address_street_name IS NOT NULL AND address_street_name != ''
|
||||
OR address_house_number IS NOT NULL AND address_house_number != ''
|
||||
)
|
||||
""")
|
||||
total = count_row["cnt"]
|
||||
logger.info(f" → Found {total} provider(s) to migrate")
|
||||
logger.info("")
|
||||
|
||||
if total == 0:
|
||||
logger.info("✅ No providers need migration. Exiting.")
|
||||
return
|
||||
|
||||
# ── Step 2: Fetch all providers needing migration ──
|
||||
logger.info("📋 Step 2: Fetching provider records")
|
||||
rows = await conn.fetch("""
|
||||
SELECT id, name, city, address_zip,
|
||||
address_street_name, address_street_type, address_house_number,
|
||||
plus_code
|
||||
FROM marketplace.service_providers
|
||||
WHERE address_id IS NULL
|
||||
AND (
|
||||
city IS NOT NULL AND city != ''
|
||||
OR address_zip IS NOT NULL AND address_zip != ''
|
||||
OR address_street_name IS NOT NULL AND address_street_name != ''
|
||||
OR address_house_number IS NOT NULL AND address_house_number != ''
|
||||
)
|
||||
ORDER BY id
|
||||
""")
|
||||
logger.info(f" → Fetched {len(rows)} record(s)")
|
||||
logger.info("")
|
||||
|
||||
# ── Step 3: Migrate each provider ──
|
||||
logger.info("📋 Step 3: Migrating addresses to system.addresses")
|
||||
migrated_count = 0
|
||||
error_count = 0
|
||||
skipped_count = 0
|
||||
|
||||
for i, row in enumerate(rows, start=1):
|
||||
provider_id = row["id"]
|
||||
provider_name = row["name"]
|
||||
|
||||
if not _has_address_data(row):
|
||||
logger.info(f" [{i}/{total}] Provider #{provider_id} '{provider_name}': "
|
||||
f"no address data → SKIP")
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
try:
|
||||
# Build the full_address_text
|
||||
full_text = _build_full_address_text(row)
|
||||
|
||||
# Insert into system.addresses
|
||||
new_address_id = uuid.uuid4()
|
||||
await conn.execute("""
|
||||
INSERT INTO system.addresses
|
||||
(id, street_name, street_type, house_number,
|
||||
full_address_text, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, NOW())
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
""",
|
||||
new_address_id,
|
||||
row.get("address_street_name"),
|
||||
row.get("address_street_type"),
|
||||
row.get("address_house_number"),
|
||||
full_text,
|
||||
)
|
||||
|
||||
# Resolve postal code if zip+city are present
|
||||
if row.get("address_zip") and row.get("city"):
|
||||
# Find or create GeoPostalCode
|
||||
postal = await conn.fetchrow("""
|
||||
SELECT id FROM system.geo_postal_codes
|
||||
WHERE zip_code = $1
|
||||
""", row["address_zip"])
|
||||
|
||||
if postal:
|
||||
postal_code_id = postal["id"]
|
||||
else:
|
||||
postal_code_id = await conn.fetchval("""
|
||||
INSERT INTO system.geo_postal_codes
|
||||
(zip_code, city, country_code)
|
||||
VALUES ($1, $2, 'HU')
|
||||
RETURNING id
|
||||
""", row["address_zip"], row["city"])
|
||||
|
||||
# Update the address with postal_code_id
|
||||
await conn.execute("""
|
||||
UPDATE system.addresses
|
||||
SET postal_code_id = $1
|
||||
WHERE id = $2
|
||||
""", postal_code_id, new_address_id)
|
||||
|
||||
# Update the provider with the new address_id
|
||||
await conn.execute("""
|
||||
UPDATE marketplace.service_providers
|
||||
SET address_id = $1
|
||||
WHERE id = $2
|
||||
""", new_address_id, provider_id)
|
||||
|
||||
migrated_count += 1
|
||||
|
||||
if i % BATCH_SIZE == 0 or i == total:
|
||||
logger.info(
|
||||
f" [{i}/{total}] Progress: {migrated_count} migrated, "
|
||||
f"{error_count} errors, {skipped_count} skipped"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
error_count += 1
|
||||
logger.error(
|
||||
f" [{i}/{total}] Provider #{provider_id} '{provider_name}': "
|
||||
f"ERROR - {e}"
|
||||
)
|
||||
|
||||
# ── Summary ──
|
||||
logger.info("")
|
||||
logger.info("=" * 70)
|
||||
logger.info("📊 MIGRATION SUMMARY")
|
||||
logger.info("=" * 70)
|
||||
logger.info(f" Total providers processed: {total}")
|
||||
logger.info(f" Successfully migrated: {migrated_count}")
|
||||
logger.info(f" Errors: {error_count}")
|
||||
logger.info(f" Skipped (no address data): {skipped_count}")
|
||||
|
||||
# ── Step 4: Verify ──
|
||||
logger.info("")
|
||||
logger.info("📋 Step 4: Verification")
|
||||
remaining = await conn.fetchval("""
|
||||
SELECT COUNT(*) FROM marketplace.service_providers
|
||||
WHERE address_id IS NULL
|
||||
AND (
|
||||
city IS NOT NULL AND city != ''
|
||||
OR address_zip IS NOT NULL AND address_zip != ''
|
||||
OR address_street_name IS NOT NULL AND address_street_name != ''
|
||||
OR address_house_number IS NOT NULL AND address_house_number != ''
|
||||
)
|
||||
""")
|
||||
total_providers = await conn.fetchval(
|
||||
"SELECT COUNT(*) FROM marketplace.service_providers"
|
||||
)
|
||||
with_address = await conn.fetchval(
|
||||
"SELECT COUNT(*) FROM marketplace.service_providers WHERE address_id IS NOT NULL"
|
||||
)
|
||||
logger.info(f" Total providers: {total_providers}")
|
||||
logger.info(f" Providers WITH address_id: {with_address}")
|
||||
logger.info(f" Providers still NULL: {remaining}")
|
||||
logger.info("")
|
||||
logger.info("✅ Phase 1 migration complete!")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"💥 FATAL ERROR: {e}", exc_info=True)
|
||||
sys.exit(1)
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
90
backend/app/scripts/seed_dismantler_category.py
Normal file
90
backend/app/scripts/seed_dismantler_category.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
Seed script: Beszúrja az "Autóbontó / Használt alkatrész" kategóriát
|
||||
a marketplace.expertise_tags táblába, ha még nem létezik.
|
||||
|
||||
Idempotens - ON CONFLICT DO NOTHING miatt többször is futtatható.
|
||||
|
||||
Futtatás: docker exec sf_api python3 /app/backend/app/scripts/seed_dismantler_category.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__)
|
||||
|
||||
DISMANTLER_CATEGORY = {
|
||||
"key": "autobonto_hasznalt_alkatresz",
|
||||
# --- 🏗️ i18n JSONB Migration (Phase 3) ---
|
||||
"name_i18n": {"hu": "Autóbontó / Használt alkatrész", "en": "Car Dismantler / Used Parts"},
|
||||
"name_hu": "Autóbontó / Használt alkatrész",
|
||||
"name_en": "Car Dismantler / Used Parts",
|
||||
"category": "parts",
|
||||
"search_keywords": [
|
||||
"bontó", "autóbontó", "bontás", "használt alkatrész",
|
||||
"dismantler", "used parts", "scrap yard", "break yard",
|
||||
"alkatrész bontás", "roncs", "roncsautó",
|
||||
],
|
||||
"description": "Használt gépjármű alkatrészek értékesítése, autóbontás, roncsautó felvásárlás",
|
||||
}
|
||||
|
||||
|
||||
async def seed_dismantler_category():
|
||||
"""Beszúrja az Autóbontó kategóriát, ha még nem létezik."""
|
||||
async with AsyncSessionLocal() as db:
|
||||
try:
|
||||
# Ellenőrizzük, hogy létezik-e már
|
||||
stmt = select(ExpertiseTag).where(
|
||||
ExpertiseTag.key == DISMANTLER_CATEGORY["key"]
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
existing = result.scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
print(
|
||||
f"✅ Az 'Autóbontó / Használt alkatrész' kategória "
|
||||
f"már létezik (ID: {existing.id}). Nincs szükség seed-elésre."
|
||||
)
|
||||
return
|
||||
|
||||
# Beszúrás
|
||||
tag = ExpertiseTag(
|
||||
key=DISMANTLER_CATEGORY["key"],
|
||||
# --- 🏗️ i18n JSONB Migration (Phase 3) ---
|
||||
name_i18n=DISMANTLER_CATEGORY.get("name_i18n", {"hu": DISMANTLER_CATEGORY["name_hu"], "en": DISMANTLER_CATEGORY["name_en"]}),
|
||||
name_hu=DISMANTLER_CATEGORY["name_hu"],
|
||||
name_en=DISMANTLER_CATEGORY["name_en"],
|
||||
category=DISMANTLER_CATEGORY["category"],
|
||||
search_keywords=DISMANTLER_CATEGORY["search_keywords"],
|
||||
description=DISMANTLER_CATEGORY["description"],
|
||||
is_official=True,
|
||||
discovery_points=10,
|
||||
usage_count=0,
|
||||
)
|
||||
db.add(tag)
|
||||
await db.commit()
|
||||
await db.refresh(tag)
|
||||
print(
|
||||
f"✅ Sikeresen beszúrva az 'Autóbontó / Használt alkatrész' "
|
||||
f"kategória (ID: {tag.id})."
|
||||
)
|
||||
|
||||
# Ellenőrzés
|
||||
count_result = await db.execute(
|
||||
select(func.count(ExpertiseTag.id))
|
||||
)
|
||||
final_count = 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_dismantler_category())
|
||||
@@ -16,6 +16,8 @@ logger = logging.getLogger(__name__)
|
||||
BASE_CATEGORIES = [
|
||||
{
|
||||
"key": "auto_szerelo",
|
||||
# --- 🏗️ i18n JSONB Migration (Phase 3) ---
|
||||
"name_i18n": {"hu": "Autószerelő", "en": "Car Mechanic"},
|
||||
"name_hu": "Autószerelő",
|
||||
"name_en": "Car Mechanic",
|
||||
"category": "vehicle_service",
|
||||
@@ -24,6 +26,8 @@ BASE_CATEGORIES = [
|
||||
},
|
||||
{
|
||||
"key": "motor_szerelo",
|
||||
# --- 🏗️ i18n JSONB Migration (Phase 3) ---
|
||||
"name_i18n": {"hu": "Motorkerékpár szerelő", "en": "Motorcycle Mechanic"},
|
||||
"name_hu": "Motorkerékpár szerelő",
|
||||
"name_en": "Motorcycle Mechanic",
|
||||
"category": "vehicle_service",
|
||||
@@ -32,6 +36,8 @@ BASE_CATEGORIES = [
|
||||
},
|
||||
{
|
||||
"key": "gumiszerviz",
|
||||
# --- 🏗️ i18n JSONB Migration (Phase 3) ---
|
||||
"name_i18n": {"hu": "Gumiszerviz", "en": "Tire Service"},
|
||||
"name_hu": "Gumiszerviz",
|
||||
"name_en": "Tire Service",
|
||||
"category": "vehicle_service",
|
||||
@@ -40,6 +46,8 @@ BASE_CATEGORIES = [
|
||||
},
|
||||
{
|
||||
"key": "karosszerialakatos",
|
||||
# --- 🏗️ i18n JSONB Migration (Phase 3) ---
|
||||
"name_i18n": {"hu": "Karosszérialakatos", "en": "Body Shop"},
|
||||
"name_hu": "Karosszérialakatos",
|
||||
"name_en": "Body Shop",
|
||||
"category": "body_paint",
|
||||
@@ -48,6 +56,8 @@ BASE_CATEGORIES = [
|
||||
},
|
||||
{
|
||||
"key": "fényező",
|
||||
# --- 🏗️ i18n JSONB Migration (Phase 3) ---
|
||||
"name_i18n": {"hu": "Fényező", "en": "Painter"},
|
||||
"name_hu": "Fényező",
|
||||
"name_en": "Painter",
|
||||
"category": "body_paint",
|
||||
@@ -56,6 +66,8 @@ BASE_CATEGORIES = [
|
||||
},
|
||||
{
|
||||
"key": "autómentő",
|
||||
# --- 🏗️ i18n JSONB Migration (Phase 3) ---
|
||||
"name_i18n": {"hu": "Autómentő / Motormentő", "en": "Tow Truck / Roadside Assistance"},
|
||||
"name_hu": "Autómentő / Motormentő",
|
||||
"name_en": "Tow Truck / Roadside Assistance",
|
||||
"category": "roadside",
|
||||
@@ -64,6 +76,8 @@ BASE_CATEGORIES = [
|
||||
},
|
||||
{
|
||||
"key": "benzinkút",
|
||||
# --- 🏗️ i18n JSONB Migration (Phase 3) ---
|
||||
"name_i18n": {"hu": "Benzinkút", "en": "Gas Station"},
|
||||
"name_hu": "Benzinkút",
|
||||
"name_en": "Gas Station",
|
||||
"category": "fuel",
|
||||
@@ -72,6 +86,8 @@ BASE_CATEGORIES = [
|
||||
},
|
||||
{
|
||||
"key": "alkatrész_kereskedés",
|
||||
# --- 🏗️ i18n JSONB Migration (Phase 3) ---
|
||||
"name_i18n": {"hu": "Alkatrész kereskedés", "en": "Parts Store"},
|
||||
"name_hu": "Alkatrész kereskedés",
|
||||
"name_en": "Parts Store",
|
||||
"category": "parts",
|
||||
@@ -80,6 +96,8 @@ BASE_CATEGORIES = [
|
||||
},
|
||||
{
|
||||
"key": "vizsgaállomás",
|
||||
# --- 🏗️ i18n JSONB Migration (Phase 3) ---
|
||||
"name_i18n": {"hu": "Vizsgaállomás", "en": "Inspection Station"},
|
||||
"name_hu": "Vizsgaállomás",
|
||||
"name_en": "Inspection Station",
|
||||
"category": "inspection",
|
||||
@@ -88,6 +106,8 @@ BASE_CATEGORIES = [
|
||||
},
|
||||
{
|
||||
"key": "autókozmetika",
|
||||
# --- 🏗️ i18n JSONB Migration (Phase 3) ---
|
||||
"name_i18n": {"hu": "Autókozmetika", "en": "Car Detailing"},
|
||||
"name_hu": "Autókozmetika",
|
||||
"name_en": "Car Detailing",
|
||||
"category": "detailing",
|
||||
@@ -96,6 +116,8 @@ BASE_CATEGORIES = [
|
||||
},
|
||||
{
|
||||
"key": "egyéb",
|
||||
# --- 🏗️ i18n JSONB Migration (Phase 3) ---
|
||||
"name_i18n": {"hu": "Egyéb szolgáltató", "en": "Other Service"},
|
||||
"name_hu": "Egyéb szolgáltató",
|
||||
"name_en": "Other Service",
|
||||
"category": "other",
|
||||
@@ -123,6 +145,8 @@ async def seed_expertise_tags():
|
||||
for cat in BASE_CATEGORIES:
|
||||
tag = ExpertiseTag(
|
||||
key=cat["key"],
|
||||
# --- 🏗️ i18n JSONB Migration (Phase 3) ---
|
||||
name_i18n=cat.get("name_i18n", {"hu": cat["name_hu"], "en": cat["name_en"]}),
|
||||
name_hu=cat["name_hu"],
|
||||
name_en=cat["name_en"],
|
||||
category=cat["category"],
|
||||
|
||||
129
backend/app/scripts/seed_gas_station.py
Normal file
129
backend/app/scripts/seed_gas_station.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""
|
||||
Seed script: Beszúrja a hiányzó "Shop / Kisbolt" (Convenience Store) kategóriát
|
||||
a marketplace.expertise_tags táblába, az "Üzemanyag és Töltőállomás" (ID=755)
|
||||
Level 1 szülő alá.
|
||||
|
||||
Meglévő kategóriák (már léteznek, nem hozzuk létre újra):
|
||||
- ID=755: Üzemanyag és Töltőállomás (Level 1)
|
||||
- ID=756: Benzinkút (Level 2)
|
||||
- ID=757: Elektromos Töltőállomás (Level 2)
|
||||
|
||||
Hiányzó kategória (ezt hozza létre a script):
|
||||
- Shop / Kisbolt (Level 2, parent_id=755)
|
||||
|
||||
Idempotens - ON CONFLICT DO NOTHING miatt többször is futtatható.
|
||||
|
||||
Futtatás: docker exec sf_api python3 /app/backend/app/scripts/seed_gas_station.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from sqlalchemy import select, func, text
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from app.models.marketplace.service import ExpertiseTag
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# A meglévő "Üzemanyag és Töltőállomás" szülő ID-ja
|
||||
FUEL_STATION_PARENT_ID = 755
|
||||
|
||||
CATEGORIES_TO_SEED = [
|
||||
{
|
||||
"key": "shop_kisbolt",
|
||||
# --- 🏗️ i18n JSONB Migration (Phase 3) ---
|
||||
"name_i18n": {"hu": "Shop / Kisbolt", "en": "Shop / Convenience Store"},
|
||||
"name_hu": "Shop / Kisbolt",
|
||||
"name_en": "Shop / Convenience Store",
|
||||
"category": "fuel",
|
||||
"level": 2,
|
||||
"parent_id": FUEL_STATION_PARENT_ID,
|
||||
"path": "uzemanyag_toltoallomas/shop_kisbolt",
|
||||
"search_keywords": [
|
||||
"shop", "kisbolt", "convenience", "vegyesbolt",
|
||||
"benzinkút shop", "élelmiszer", "snack", "ital",
|
||||
],
|
||||
"description": "Benzinkúton üzemelő kisbolt, shop, convenience store",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def seed_gas_station_categories():
|
||||
"""Beszúrja a hiányzó kategóriákat, ha még nem léteznek."""
|
||||
async with AsyncSessionLocal() as db:
|
||||
try:
|
||||
for cat in CATEGORIES_TO_SEED:
|
||||
# Ellenőrizzük, hogy létezik-e már
|
||||
stmt = select(ExpertiseTag).where(
|
||||
ExpertiseTag.key == cat["key"]
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
existing = result.scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
print(
|
||||
f"✅ A '{cat['name_hu']}' kategória "
|
||||
f"már létezik (ID: {existing.id}). Nincs szükség seed-elésre."
|
||||
)
|
||||
continue
|
||||
|
||||
# Beszúrás
|
||||
tag = ExpertiseTag(
|
||||
key=cat["key"],
|
||||
# --- 🏗️ i18n JSONB Migration (Phase 3) ---
|
||||
name_i18n=cat.get("name_i18n", {"hu": cat["name_hu"], "en": cat["name_en"]}),
|
||||
name_hu=cat["name_hu"],
|
||||
name_en=cat["name_en"],
|
||||
category=cat["category"],
|
||||
level=cat["level"],
|
||||
parent_id=cat["parent_id"],
|
||||
path=cat["path"],
|
||||
search_keywords=cat["search_keywords"],
|
||||
description=cat["description"],
|
||||
is_official=True,
|
||||
discovery_points=10,
|
||||
usage_count=0,
|
||||
)
|
||||
db.add(tag)
|
||||
await db.flush()
|
||||
await db.refresh(tag)
|
||||
print(
|
||||
f"✅ Sikeresen beszúrva a '{cat['name_hu']}' "
|
||||
f"kategória (ID: {tag.id})."
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
|
||||
# Ellenőrzés
|
||||
count_result = await db.execute(
|
||||
select(func.count(ExpertiseTag.id))
|
||||
)
|
||||
final_count = count_result.scalar()
|
||||
print(f"📊 Összes rekord az expertise_tags táblában: {final_count}")
|
||||
|
||||
# Listázzuk a benzinkút kategóriákat
|
||||
print("\n📋 Benzinkút kategóriák:")
|
||||
result = await db.execute(
|
||||
text("""
|
||||
SELECT id, key, name_hu, name_en, level, parent_id, path
|
||||
FROM marketplace.expertise_tags
|
||||
WHERE parent_id = :pid OR id = :pid
|
||||
ORDER BY level, id
|
||||
"""),
|
||||
{"pid": FUEL_STATION_PARENT_ID},
|
||||
)
|
||||
for row in result:
|
||||
print(f" ID={row.id}, key={row.key}, name_hu={row.name_hu}, level={row.level}")
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"Hiba a seed-elés során: {e}")
|
||||
print(f"❌ Hiba: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def main():
|
||||
asyncio.run(seed_gas_station_categories())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
62
backend/app/scripts/seed_validation_rules.py
Normal file
62
backend/app/scripts/seed_validation_rules.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/scripts/seed_validation_rules.py
|
||||
"""
|
||||
Seed script for gamification.validation_rules table.
|
||||
|
||||
Inserts the base validation rules required by the P0 Architecture:
|
||||
|
||||
BOT_BASE_SCORE = 20 # Default trust score for robot-discovered entries
|
||||
FIRST_ENTRY_SCORE = 40 # Score awarded for the first user entry
|
||||
USER_CONFIRM_BASE = 20 # Base score per user confirmation vote
|
||||
AUTO_APPROVE_THRESHOLD = 80 # Minimum validation_level to auto-promote
|
||||
|
||||
Usage:
|
||||
docker exec -it sf_api python -m app.scripts.seed_validation_rules
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||
|
||||
from app.database import AsyncSessionLocal
|
||||
from app.models.gamification.validation_rule import ValidationRule
|
||||
from sqlalchemy import select
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(name)s: %(message)s')
|
||||
logger = logging.getLogger("SeedValidationRules")
|
||||
|
||||
BASE_RULES = [
|
||||
{"rule_key": "BOT_BASE_SCORE", "rule_value": 20, "description": "Default trust score for robot-discovered service entries"},
|
||||
{"rule_key": "FIRST_ENTRY_SCORE", "rule_value": 40, "description": "Score awarded for the first user-submitted service entry"},
|
||||
{"rule_key": "USER_CONFIRM_BASE", "rule_value": 20, "description": "Base score per user confirmation vote (level multiplier added)"},
|
||||
{"rule_key": "AUTO_APPROVE_THRESHOLD", "rule_value": 80, "description": "Minimum validation_level required for auto-promotion to provider"},
|
||||
]
|
||||
|
||||
|
||||
async def seed():
|
||||
async with AsyncSessionLocal() as db:
|
||||
for rule_data in BASE_RULES:
|
||||
# Check if rule already exists
|
||||
stmt = select(ValidationRule).where(ValidationRule.rule_key == rule_data["rule_key"])
|
||||
result = await db.execute(stmt)
|
||||
existing = result.scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
logger.info(f"Rule '{rule_data['rule_key']}' already exists (value={existing.rule_value}), skipping.")
|
||||
continue
|
||||
|
||||
rule = ValidationRule(
|
||||
rule_key=rule_data["rule_key"],
|
||||
rule_value=rule_data["rule_value"],
|
||||
description=rule_data["description"],
|
||||
)
|
||||
db.add(rule)
|
||||
logger.info(f"Created rule '{rule_data['rule_key']}' = {rule_data['rule_value']}")
|
||||
|
||||
await db.commit()
|
||||
logger.info("✅ Validation rules seeded successfully.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(seed())
|
||||
Reference in New Issue
Block a user