#!/usr/bin/env python3 """ P0 HYBRID VENDOR REFACTOR: Data Migration Script Migrates existing crowdsourced service provider organizations to the new marketplace.service_providers table. Safe List (valódi garázsok, amelyek NEM kerülnek migrálásra): (15, 21, 43, 45, 47, 48, 49, 50, 66, 67, 69, 70, 71) Folyamat: 1. Lekérdezi az összes Organization-t, ahol org_type='service_provider' és az id NINCS a safe list-ben. 2. Minden ilyen Organization-hoz létrehoz egy ServiceProvider rekordot. 3. Frissíti a ServiceProfile-okat: service_provider_id beállítása. 4. Frissíti az AssetCost-okat: service_provider_id beállítása. 5. Jelentés a migrációról. Használat: docker compose exec sf_api python3 /app/scripts/migrate_providers.py """ import asyncio import logging import sys from datetime import datetime, timezone from sqlalchemy import select, text from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from app.core.config import settings # ────────────────────────────────────────────── # Konfiguráció # ────────────────────────────────────────────── # Safe list: ezek az Organization ID-k valódi garázsok, NEM migráljuk őket SAFE_ORG_IDS = (15, 21, 43, 45, 47, 48, 49, 50, 66, 67, 69, 70, 71) # Adatbázis URL a settings-ből DATABASE_URL = settings.DATABASE_URL logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", handlers=[logging.StreamHandler(sys.stdout)], ) logger = logging.getLogger(__name__) async def migrate_providers(): """ Main migration logic. """ engine = create_async_engine(DATABASE_URL, echo=False) async with AsyncSession(engine) as db: # ── 1. Keresd meg a migrálható Organization-öket ── logger.info("🔍 Searching for service_provider organizations outside safe list...") # Build safe_ids placeholder list for asyncpg safe_list = ", ".join([str(x) for x in SAFE_ORG_IDS]) sql = f""" SELECT id, name, display_name, address_city, address_zip, address_street_name, address_street_type, address_house_number, plus_code, owner_id, created_at FROM fleet.organizations WHERE org_type = 'service_provider' AND id NOT IN ({safe_list}) """ result = await db.execute(text(sql)) orgs = result.fetchall() logger.info(f"📊 Found {len(orgs)} organizations to migrate.") if not orgs: logger.info("✅ No organizations to migrate. Nothing to do.") return # ── 2. Minden Organization-hoz hozz létre egy ServiceProvider-t ── migrated_count = 0 skipped_count = 0 error_count = 0 for org in orgs: org_id = org[0] org_name = org[1] try: # Ellenőrizd, hogy már létezik-e ServiceProvider ehhez az org-hoz check_stmt = select(text("id")).select_from( text("marketplace.service_providers") ).where(text("name = :name")).params(name=org_name) check_result = await db.execute(check_stmt) existing = check_result.fetchone() if existing: logger.warning(f"⚠️ ServiceProvider already exists for org_id={org_id} (name={org_name}), skipping...") skipped_count += 1 continue # Összeállítjuk a címet street_parts = [p for p in [org[5], org[6], org[7]] if p] address = ", ".join(street_parts) if street_parts else org_name # ServiceProvider létrehozása insert_sp = text(""" INSERT INTO marketplace.service_providers (name, address, city, address_zip, address_street_name, address_street_type, address_house_number, plus_code, status, source, validation_score, added_by_user_id, created_at) VALUES (:name, :address, :city, :zip, :street_name, :street_type, :house_number, :plus_code, 'pending', 'manual', 50, :added_by, :created_at) RETURNING id """) sp_result = await db.execute( insert_sp, { "name": org_name, "address": address, "city": org[3], "zip": org[4], "street_name": org[5], "street_type": org[6], "house_number": org[7], "plus_code": org[8], "added_by": org[9], "created_at": org[10] or datetime.now(timezone.utc), } ) sp_id = sp_result.scalar() # ── 3. Frissítsd a ServiceProfile-okat ── update_profile = text(""" UPDATE marketplace.service_profiles SET service_provider_id = :sp_id WHERE organization_id = :org_id """) await db.execute(update_profile, {"sp_id": sp_id, "org_id": org_id}) # ── 4. Frissítsd az AssetCost-okat ── update_cost = text(""" UPDATE fleet_finance.asset_costs SET service_provider_id = :sp_id WHERE vendor_organization_id = :org_id """) await db.execute(update_cost, {"sp_id": sp_id, "org_id": org_id}) await db.commit() migrated_count += 1 logger.info(f"✅ Migrated org_id={org_id} (name={org_name}) -> service_provider_id={sp_id}") except Exception as e: await db.rollback() error_count += 1 logger.error(f"❌ Error migrating org_id={org_id} (name={org_name}): {e}") # ── 5. Jelentés ── logger.info("=" * 60) logger.info("📋 MIGRATION REPORT") logger.info("=" * 60) logger.info(f" Total organizations found: {len(orgs)}") logger.info(f" Successfully migrated: {migrated_count}") logger.info(f" Skipped (already exists): {skipped_count}") logger.info(f" Errors: {error_count}") logger.info("=" * 60) if error_count > 0: logger.warning("⚠️ Some migrations failed. Check logs above for details.") else: logger.info("✅ Migration completed successfully!") async def main(): logger.info("🚀 P0 Hybrid Vendor Refactor - Data Migration Script") logger.info(f" Safe org IDs: {SAFE_ORG_IDS}") logger.info(f" Database: {DATABASE_URL}") logger.info("") try: await migrate_providers() except Exception as e: logger.error(f"💥 Fatal error: {e}") sys.exit(1) if __name__ == "__main__": asyncio.run(main())