frontend admin refakctorálás

This commit is contained in:
Roo
2026-07-08 08:03:57 +00:00
parent 07b59032ce
commit 2e0abc62a7
455 changed files with 14428 additions and 21651 deletions

View 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())

View 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())

View 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())

View File

@@ -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"],

View 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()

View 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())