#!/usr/bin/env python3 """ 🔐 SAFE RENAME GARAGES — Private Garage Display Name Normalization This script targets private garages (organizations with org_type='individual' or those lacking a tax_number, representing individuals rather than corporate entities). What it does: 1. Finds all private/individual garages in fleet.organizations 2. Sets `name` to: "{last_name} {first_name} - Privát Garázs (#{user_id})" (internal name with ID for uniqueness) 3. Sets `display_name` to: "{last_name} {first_name} - Privát Garázs" (clean display name without exposing the internal ID) Strict Rules: - READ/UPDATE only — NO DELETES - Does NOT touch corporate/business organizations - Does NOT modify any user records - Preserves all existing data relationships Usage: docker compose exec sf_api python3 /app/scripts/safe_rename_garages.py """ import asyncio import logging import os import sys from sqlalchemy import text from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", handlers=[logging.StreamHandler(sys.stdout)], ) logger = logging.getLogger("safe_rename_garages") async def main(): """Main execution: find private garages and update their names.""" # Use the same DATABASE_URL as the application database_url = os.environ.get( "DATABASE_URL", "postgresql+asyncpg://service_finder_app:AppSafePass_2026@shared-postgres:5432/service_finder", ) engine = create_async_engine(database_url, echo=False) async with engine.connect() as conn: # ── Step 1: Find private/individual garages ── logger.info("🔍 Scanning for private garages (individual org_type)...") result = await conn.execute( text(""" SELECT o.id, o.name, o.full_name, o.display_name, o.org_type, o.tax_number, o.owner_id, o.legal_owner_id, p.last_name, p.first_name, u.email as owner_email FROM fleet.organizations o LEFT JOIN identity.users u ON u.id = o.owner_id LEFT JOIN identity.persons p ON p.id = COALESCE(o.legal_owner_id, u.person_id) WHERE (o.org_type = 'individual' OR o.org_type IS NULL) AND o.is_deleted = false ORDER BY o.id """) ) rows = result.fetchall() logger.info(f"📊 Found {len(rows)} private garage(s) to process") if not rows: logger.info("✅ No private garages found. Nothing to do.") return updated_count = 0 skipped_count = 0 for row in rows: org_id = row[0] current_name = row[1] current_full_name = row[2] current_display_name = row[3] org_type = row[4] tax_number = row[5] owner_id = row[6] legal_owner_id = row[7] last_name = row[8] first_name = row[9] owner_email = row[10] # Determine the person's name person_name = None if last_name and first_name: person_name = f"{last_name} {first_name}" elif current_full_name: person_name = current_full_name elif owner_email: person_name = owner_email.split("@")[0] else: person_name = f"User #{owner_id or '?'}" # Use owner_id for uniqueness in the internal name uid = owner_id or legal_owner_id or org_id # Build the new names new_internal_name = f"{person_name} - Privát Garázs (#{uid})" new_display_name = f"{person_name} - Privát Garázs" # Check if update is needed if current_name == new_internal_name and current_display_name == new_display_name: logger.info(f" ⏭️ Org #{org_id}: already up-to-date ('{new_display_name}')") skipped_count += 1 continue # ── Step 2: Update the organization ── logger.info(f" ✏️ Org #{org_id}: '{current_name or 'N/A'}' → '{new_internal_name}'") logger.info(f" Display: '{current_display_name or 'N/A'}' → '{new_display_name}'") await conn.execute( text(""" UPDATE fleet.organizations SET name = :new_name, display_name = :new_display_name, updated_at = NOW() WHERE id = :org_id """), { "new_name": new_internal_name, "new_display_name": new_display_name, "org_id": org_id, } ) updated_count += 1 # ── Commit all changes ── await conn.commit() logger.info(f"✅ Done! Updated: {updated_count}, Skipped: {skipped_count}") # ── Step 3: Verification ── logger.info("🔍 Verifying updates...") verify_result = await conn.execute( text(""" SELECT id, name, display_name, org_type FROM fleet.organizations WHERE org_type = 'individual' OR org_type IS NULL ORDER BY id """) ) verify_rows = verify_result.fetchall() for vrow in verify_rows: logger.info(f" Org #{vrow[0]}: name='{vrow[1]}', display_name='{vrow[2]}', type='{vrow[3]}'") await engine.dispose() logger.info("🎉 Safe rename completed successfully!") if __name__ == "__main__": asyncio.run(main())