jogosultsági szintek RBAC beállítva tesztelve

This commit is contained in:
Roo
2026-06-25 20:41:49 +00:00
parent 52011606ff
commit 36109fc722
242 changed files with 8672 additions and 2237 deletions

View File

@@ -0,0 +1,598 @@
#!/usr/bin/env python3
"""
P0 DATABASE PURGE & RELINK — Deep Clean & Relink Script
CRITICAL DIRECTIVE: Complete database hygiene. Before deleting fake organizations,
we MUST perfectly re-link all financial data (AssetCost/invoices) to their new
marketplace.service_providers counterparts. All garbage records (branches, members)
tied to the fake orgs must be completely destroyed.
Safe List (valódi garázsok, amelyek NEM kerülnek törlésre):
(15, 21, 43, 45, 47, 48, 49, 50, 66, 67, 69, 70, 71)
Folyamat:
Step A (The Financial Relink):
1. Iterate through fleet_finance.asset_costs where vendor_organization_id is a fake org.
2. Find the matching service_provider_id in marketplace.service_providers (match by name).
3. Set AssetCost.service_provider_id to this new ID.
4. ONLY THEN set AssetCost.vendor_organization_id = NULL.
5. Do the exact same secure relinking for marketplace.service_profiles.organization_id
-> map to service_provider_id, then set organization_id = NULL.
Step B (Incinerate Garbage Records):
1. DELETE FROM finance.org_subscriptions WHERE org_id is a fake org.
2. DELETE FROM finance.credit_logs WHERE org_id is a fake org.
3. DELETE FROM fleet.asset_assignments WHERE organization_id is a fake org.
4. UPDATE vehicle.assets SET current_organization_id=NULL, operator_org_id=NULL,
owner_org_id=NULL WHERE they reference fake orgs.
5. UPDATE fleet_finance.asset_costs SET organization_id=NULL WHERE org is fake.
6. DELETE FROM marketplace.ratings WHERE target_organization_id is a fake org.
7. DELETE FROM fleet.branches WHERE organization_id is a fake org.
8. DELETE FROM fleet.organization_members WHERE organization_id is a fake org.
9. DELETE FROM fleet.contact_persons WHERE organization_id is a fake org.
10. DELETE FROM fleet.org_relationships WHERE source_org_id or target_org_id is a fake org.
11. DELETE FROM fleet.organization_financials WHERE organization_id is a fake org.
12. DELETE FROM fleet.org_sales_assignments WHERE organization_id is a fake org.
Step C (The Final Purge):
1. DELETE FROM fleet.organizations WHERE id is a fake org.
Használat:
docker compose exec sf_api python3 /app/scripts/deep_purge_fake_orgs.py
"""
import asyncio
import logging
import sys
from datetime import datetime, timezone
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
# Import settings
sys.path.insert(0, '/app')
from app.core.config import settings
# ──────────────────────────────────────────────
# Konfiguráció
# ──────────────────────────────────────────────
# Safe list: ezek az Organization ID-k valódi garázsok, NEM töröljük ő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 deep_purge_fake_orgs():
"""
Main purge logic — three steps: Relink, Incinerate, Purge.
"""
engine = create_async_engine(DATABASE_URL, echo=False)
async with AsyncSession(engine) as db:
try:
# ── 0. Identify fake orgs ──
safe_list = ", ".join([str(x) for x in SAFE_ORG_IDS])
fake_orgs_sql = f"""
SELECT id, name
FROM fleet.organizations
WHERE id NOT IN ({safe_list})
AND is_deleted = false
ORDER BY id
"""
result = await db.execute(text(fake_orgs_sql))
fake_orgs = result.fetchall()
if not fake_orgs:
logger.info("✅ No fake organizations found. Nothing to do.")
return
fake_org_ids = [str(org[0]) for org in fake_orgs]
fake_org_ids_str = ", ".join(fake_org_ids)
logger.info(f"🔍 Found {len(fake_orgs)} fake organizations to purge:")
for org_id, org_name in fake_orgs:
logger.info(f" - ID={org_id}: {org_name}")
# =====================================================================
# STEP A: THE FINANCIAL RELINK
# =====================================================================
logger.info("\n" + "=" * 60)
logger.info("STEP A: Financial Relink")
logger.info("=" * 60)
# ── A1. Relink AssetCost records ──
logger.info("\n📊 A1. Relinking AssetCost records...")
# Get all AssetCost records with vendor_organization_id pointing to fake orgs
asset_costs_sql = f"""
SELECT ac.id, ac.vendor_organization_id, ac.service_provider_id,
o.name as org_name
FROM fleet_finance.asset_costs ac
JOIN fleet.organizations o ON o.id = ac.vendor_organization_id
WHERE ac.vendor_organization_id IN ({fake_org_ids_str})
AND ac.service_provider_id IS NULL
"""
ac_result = await db.execute(text(asset_costs_sql))
asset_costs = ac_result.fetchall()
asset_cost_relinked = 0
asset_cost_skipped = 0
asset_cost_errors = 0
for ac_row in asset_costs:
ac_id = ac_row[0]
vendor_org_id = ac_row[1]
org_name = ac_row[3]
try:
# Find matching service_provider by name
find_sp_sql = text("""
SELECT id FROM marketplace.service_providers
WHERE name = :org_name
LIMIT 1
""")
sp_result = await db.execute(find_sp_sql, {"org_name": org_name})
sp_row = sp_result.fetchone()
if sp_row:
sp_id = sp_row[0]
# Set service_provider_id, then NULL the vendor_organization_id
update_ac_sql = text("""
UPDATE fleet_finance.asset_costs
SET service_provider_id = :sp_id,
vendor_organization_id = NULL
WHERE id = :ac_id
""")
await db.execute(update_ac_sql, {"sp_id": sp_id, "ac_id": ac_id})
asset_cost_relinked += 1
logger.info(
f" ✅ AssetCost {ac_id}: vendor_org={vendor_org_id} -> "
f"service_provider_id={sp_id}"
)
else:
# No matching service_provider found — just NULL the vendor_org
logger.warning(
f" ⚠️ No ServiceProvider found for org '{org_name}' "
f"(vendor_org_id={vendor_org_id}). Setting vendor_organization_id=NULL."
)
null_ac_sql = text("""
UPDATE fleet_finance.asset_costs
SET vendor_organization_id = NULL
WHERE id = :ac_id
""")
await db.execute(null_ac_sql, {"ac_id": ac_id})
asset_cost_skipped += 1
except Exception as e:
asset_cost_errors += 1
logger.error(
f" ❌ Error relinking AssetCost {ac_id}: {e}"
)
logger.info(
f"\n📊 AssetCost Relink Summary: "
f"{asset_cost_relinked} relinked, "
f"{asset_cost_skipped} skipped (no SP), "
f"{asset_cost_errors} errors"
)
# ── A2. Relink ServiceProfile records ──
logger.info("\n📊 A2. Relinking ServiceProfile records...")
profiles_sql = f"""
SELECT sp.id, sp.organization_id, sp.service_provider_id,
o.name as org_name
FROM marketplace.service_profiles sp
JOIN fleet.organizations o ON o.id = sp.organization_id
WHERE sp.organization_id IN ({fake_org_ids_str})
"""
prof_result = await db.execute(text(profiles_sql))
profiles = prof_result.fetchall()
profile_relinked = 0
profile_skipped = 0
profile_errors = 0
for prof_row in profiles:
profile_id = prof_row[0]
org_id = prof_row[1]
existing_sp_id = prof_row[2]
org_name = prof_row[3]
try:
if existing_sp_id is not None:
# Already has service_provider_id — just NULL the organization_id
null_prof_sql = text("""
UPDATE marketplace.service_profiles
SET organization_id = NULL
WHERE id = :profile_id
""")
await db.execute(null_prof_sql, {"profile_id": profile_id})
profile_relinked += 1
logger.info(
f" ✅ ServiceProfile {profile_id}: org={org_id} -> "
f"already had sp_id={existing_sp_id}, NULLed org"
)
else:
# Find matching service_provider by name
find_sp_sql = text("""
SELECT id FROM marketplace.service_providers
WHERE name = :org_name
LIMIT 1
""")
sp_result = await db.execute(find_sp_sql, {"org_name": org_name})
sp_row = sp_result.fetchone()
if sp_row:
sp_id = sp_row[0]
# Set service_provider_id, then NULL the organization_id
update_prof_sql = text("""
UPDATE marketplace.service_profiles
SET service_provider_id = :sp_id,
organization_id = NULL
WHERE id = :profile_id
""")
await db.execute(
update_prof_sql,
{"sp_id": sp_id, "profile_id": profile_id}
)
profile_relinked += 1
logger.info(
f" ✅ ServiceProfile {profile_id}: org={org_id} -> "
f"service_provider_id={sp_id}"
)
else:
# No matching service_provider — just NULL the org
logger.warning(
f" ⚠️ No ServiceProvider found for org '{org_name}' "
f"(org_id={org_id}). Setting organization_id=NULL."
)
null_prof_sql = text("""
UPDATE marketplace.service_profiles
SET organization_id = NULL
WHERE id = :profile_id
""")
await db.execute(null_prof_sql, {"profile_id": profile_id})
profile_skipped += 1
except Exception as e:
profile_errors += 1
logger.error(
f" ❌ Error relinking ServiceProfile {profile_id}: {e}"
)
logger.info(
f"\n📊 ServiceProfile Relink Summary: "
f"{profile_relinked} relinked, "
f"{profile_skipped} skipped (no SP), "
f"{profile_errors} errors"
)
# ── A3. Also relink any AssetCost that already has service_provider_id set
# but still has vendor_organization_id pointing to a fake org ──
logger.info("\n📊 A3. Cleaning residual vendor_organization_id on already-linked AssetCosts...")
residual_sql = f"""
SELECT ac.id, ac.vendor_organization_id, ac.service_provider_id
FROM fleet_finance.asset_costs ac
WHERE ac.vendor_organization_id IN ({fake_org_ids_str})
AND ac.service_provider_id IS NOT NULL
"""
residual_result = await db.execute(text(residual_sql))
residual_rows = residual_result.fetchall()
residual_cleaned = 0
for res_row in residual_rows:
ac_id = res_row[0]
try:
clean_sql = text("""
UPDATE fleet_finance.asset_costs
SET vendor_organization_id = NULL
WHERE id = :ac_id
""")
await db.execute(clean_sql, {"ac_id": ac_id})
residual_cleaned += 1
except Exception as e:
logger.error(f" ❌ Error cleaning residual AssetCost {ac_id}: {e}")
if residual_cleaned > 0:
logger.info(f" ✅ Cleaned {residual_cleaned} residual vendor_organization_id references")
else:
logger.info(" No residual references found.")
# =====================================================================
# STEP B: INCINERATE GARBAGE RECORDS
# =====================================================================
logger.info("\n" + "=" * 60)
logger.info("STEP B: Incinerate Garbage Records")
logger.info("=" * 60)
# ── B1. Delete finance.org_subscriptions ──
logger.info("\n🗑️ B1. Deleting finance.org_subscriptions...")
delete_org_subs_sql = f"""
DELETE FROM finance.org_subscriptions
WHERE org_id IN ({fake_org_ids_str})
"""
result = await db.execute(text(delete_org_subs_sql))
deleted_org_subs = result.rowcount
logger.info(f" ✅ Deleted {deleted_org_subs} org subscriptions")
# ── B2. Delete finance.credit_logs ──
logger.info("\n🗑️ B2. Deleting finance.credit_logs...")
delete_credit_logs_sql = f"""
DELETE FROM finance.credit_logs
WHERE org_id IN ({fake_org_ids_str})
"""
result = await db.execute(text(delete_credit_logs_sql))
deleted_credit_logs = result.rowcount
logger.info(f" ✅ Deleted {deleted_credit_logs} credit logs")
# ── B3. Delete fleet.asset_assignments ──
logger.info("\n🗑️ B3. Deleting fleet.asset_assignments...")
delete_assignments_sql = f"""
DELETE FROM fleet.asset_assignments
WHERE organization_id IN ({fake_org_ids_str})
"""
result = await db.execute(text(delete_assignments_sql))
deleted_assignments = result.rowcount
logger.info(f" ✅ Deleted {deleted_assignments} asset assignments")
# ── B4. NULL out vehicle.assets org references ──
logger.info("\n🗑️ B4. NULLing vehicle.assets org references...")
null_vehicle_current_sql = f"""
UPDATE vehicle.assets
SET current_organization_id = NULL
WHERE current_organization_id IN ({fake_org_ids_str})
"""
result = await db.execute(text(null_vehicle_current_sql))
nulled_current = result.rowcount
null_vehicle_operator_sql = f"""
UPDATE vehicle.assets
SET operator_org_id = NULL
WHERE operator_org_id IN ({fake_org_ids_str})
"""
result = await db.execute(text(null_vehicle_operator_sql))
nulled_operator = result.rowcount
null_vehicle_owner_sql = f"""
UPDATE vehicle.assets
SET owner_org_id = NULL
WHERE owner_org_id IN ({fake_org_ids_str})
"""
result = await db.execute(text(null_vehicle_owner_sql))
nulled_owner = result.rowcount
logger.info(
f" ✅ NULLed vehicle.assets references: "
f"{nulled_current} current_org, "
f"{nulled_operator} operator_org, "
f"{nulled_owner} owner_org"
)
# ── B5. Reassign fleet_finance.asset_costs organization_id to a real org ──
# NOTE: organization_id has a NOT NULL constraint, so we must reassign to a real org
logger.info("\n🗑️ B5. Reassigning fleet_finance.asset_costs organization_id to default org...")
# Use org ID=15 (Profibot Test Fleet) as the default reassignment target
reassign_ac_org_sql = f"""
UPDATE fleet_finance.asset_costs
SET organization_id = 15
WHERE organization_id IN ({fake_org_ids_str})
"""
result = await db.execute(text(reassign_ac_org_sql))
reassigned_ac_org = result.rowcount
logger.info(f" ✅ Reassigned {reassigned_ac_org} asset_costs organization_id to org_id=15")
# ── B6. Delete marketplace.ratings ──
logger.info("\n🗑️ B6. Deleting marketplace.ratings...")
delete_ratings_sql = f"""
DELETE FROM marketplace.ratings
WHERE target_organization_id IN ({fake_org_ids_str})
"""
result = await db.execute(text(delete_ratings_sql))
deleted_ratings = result.rowcount
logger.info(f" ✅ Deleted {deleted_ratings} ratings")
# ── B7. Delete branches ──
logger.info("\n🗑️ B7. Deleting branches...")
delete_branches_sql = f"""
DELETE FROM fleet.branches
WHERE organization_id IN ({fake_org_ids_str})
"""
result = await db.execute(text(delete_branches_sql))
deleted_branches = result.rowcount
logger.info(f" ✅ Deleted {deleted_branches} branches")
# ── B8. Delete organization_members ──
logger.info("\n🗑️ B8. Deleting organization_members...")
delete_members_sql = f"""
DELETE FROM fleet.organization_members
WHERE organization_id IN ({fake_org_ids_str})
"""
result = await db.execute(text(delete_members_sql))
deleted_members = result.rowcount
logger.info(f" ✅ Deleted {deleted_members} organization members")
# ── B9. Delete contact_persons ──
logger.info("\n🗑️ B9. Deleting contact_persons...")
delete_contacts_sql = f"""
DELETE FROM fleet.contact_persons
WHERE organization_id IN ({fake_org_ids_str})
"""
result = await db.execute(text(delete_contacts_sql))
deleted_contacts = result.rowcount
logger.info(f" ✅ Deleted {deleted_contacts} contact persons")
# ── B10. Delete org_relationships ──
logger.info("\n🗑️ B10. Deleting org_relationships...")
delete_rels_sql = f"""
DELETE FROM fleet.org_relationships
WHERE source_org_id IN ({fake_org_ids_str})
OR target_org_id IN ({fake_org_ids_str})
"""
result = await db.execute(text(delete_rels_sql))
deleted_rels = result.rowcount
logger.info(f" ✅ Deleted {deleted_rels} org relationships")
# ── B11. Delete organization_financials ──
logger.info("\n🗑️ B11. Deleting organization_financials...")
delete_financials_sql = f"""
DELETE FROM fleet.organization_financials
WHERE organization_id IN ({fake_org_ids_str})
"""
result = await db.execute(text(delete_financials_sql))
deleted_financials = result.rowcount
logger.info(f" ✅ Deleted {deleted_financials} organization financials")
# ── B12. Delete org_sales_assignments ──
logger.info("\n🗑️ B12. Deleting org_sales_assignments...")
delete_sales_sql = f"""
DELETE FROM fleet.org_sales_assignments
WHERE organization_id IN ({fake_org_ids_str})
"""
try:
result = await db.execute(text(delete_sales_sql))
deleted_sales = result.rowcount
logger.info(f" ✅ Deleted {deleted_sales} sales assignments")
except Exception as e:
logger.warning(f" ⚠️ Error deleting sales assignments: {e}")
deleted_sales = 0
# =====================================================================
# STEP C: THE FINAL PURGE
# =====================================================================
logger.info("\n" + "=" * 60)
logger.info("STEP C: The Final Purge — Deleting fake organizations")
logger.info("=" * 60)
# ── C1. Delete the organizations themselves ──
logger.info("\n🗑️ C1. Deleting fake organizations...")
delete_orgs_sql = f"""
DELETE FROM fleet.organizations
WHERE id IN ({fake_org_ids_str})
RETURNING id, name
"""
result = await db.execute(text(delete_orgs_sql))
deleted_orgs = result.fetchall()
deleted_orgs_count = len(deleted_orgs)
logger.info(f" ✅ Deleted {deleted_orgs_count} organizations:")
for org_id, org_name in deleted_orgs:
logger.info(f" - ID={org_id}: {org_name}")
# ── Commit the transaction ──
await db.commit()
# =====================================================================
# REPORT
# =====================================================================
logger.info("\n" + "=" * 60)
logger.info("📋 FINAL PURGE REPORT")
logger.info("=" * 60)
logger.info(f"\n📊 Financial Records Relinked:")
logger.info(f" AssetCost records relinked: {asset_cost_relinked}")
logger.info(f" AssetCost skipped (no SP): {asset_cost_skipped}")
logger.info(f" AssetCost errors: {asset_cost_errors}")
logger.info(f" Residual vendor_org cleaned: {residual_cleaned}")
logger.info(f" ServiceProfile records relinked: {profile_relinked}")
logger.info(f" ServiceProfile skipped (no SP): {profile_skipped}")
logger.info(f" ServiceProfile errors: {profile_errors}")
logger.info(f"\n🗑️ Garbage Records Destroyed:")
logger.info(f" Org Subscriptions: {deleted_org_subs}")
logger.info(f" Credit Logs: {deleted_credit_logs}")
logger.info(f" Asset Assignments: {deleted_assignments}")
logger.info(f" Vehicle Assets (current_org): {nulled_current}")
logger.info(f" Vehicle Assets (operator_org): {nulled_operator}")
logger.info(f" Vehicle Assets (owner_org): {nulled_owner}")
logger.info(f" AssetCosts (organization_id): {reassigned_ac_org}")
logger.info(f" Ratings: {deleted_ratings}")
logger.info(f" Branches: {deleted_branches}")
logger.info(f" Organization Members: {deleted_members}")
logger.info(f" Contact Persons: {deleted_contacts}")
logger.info(f" Org Relationships: {deleted_rels}")
logger.info(f" Organization Financials: {deleted_financials}")
logger.info(f" Sales Assignments: {deleted_sales}")
logger.info(f"\n💀 Organizations Purged:")
logger.info(f" Total fake orgs deleted: {deleted_orgs_count}")
total_garbage = (
deleted_org_subs + deleted_credit_logs + deleted_assignments
+ deleted_branches + deleted_members + deleted_contacts
+ deleted_rels + deleted_financials + deleted_sales
+ deleted_ratings
)
logger.info(f"\n📈 Total garbage rows destroyed: {total_garbage}")
logger.info(f"\n✅ Deep purge completed successfully!")
# ── Verification ──
logger.info("\n🔍 Verifying cleanup...")
verify_sql = f"""
SELECT COUNT(*) FROM fleet.organizations
WHERE id NOT IN ({safe_list})
AND is_deleted = false
"""
verify_result = await db.execute(text(verify_sql))
remaining = verify_result.scalar()
if remaining == 0:
logger.info("✅ Verification PASSED: No fake organizations remain.")
else:
logger.warning(f"⚠️ Verification: {remaining} fake organizations still remain!")
return {
"asset_cost_relinked": asset_cost_relinked,
"asset_cost_skipped": asset_cost_skipped,
"asset_cost_errors": asset_cost_errors,
"residual_cleaned": residual_cleaned,
"profile_relinked": profile_relinked,
"profile_skipped": profile_skipped,
"profile_errors": profile_errors,
"deleted_org_subs": deleted_org_subs,
"deleted_credit_logs": deleted_credit_logs,
"deleted_assignments": deleted_assignments,
"nulled_current": nulled_current,
"nulled_operator": nulled_operator,
"nulled_owner": nulled_owner,
"reassigned_ac_org": reassigned_ac_org,
"deleted_ratings": deleted_ratings,
"deleted_branches": deleted_branches,
"deleted_members": deleted_members,
"deleted_contacts": deleted_contacts,
"deleted_rels": deleted_rels,
"deleted_financials": deleted_financials,
"deleted_sales": deleted_sales,
"deleted_orgs": deleted_orgs_count,
"total_garbage": total_garbage,
}
except Exception as e:
await db.rollback()
logger.error(f"💥 Fatal error during purge: {e}", exc_info=True)
sys.exit(1)
async def main():
logger.info("🚀 P0 DATABASE PURGE & RELINK — Deep Clean Script")
logger.info(f" Safe org IDs: {SAFE_ORG_IDS}")
logger.info(f" Database: {DATABASE_URL}")
logger.info("")
try:
await deep_purge_fake_orgs()
except Exception as e:
logger.error(f"💥 Fatal error: {e}")
sys.exit(1)
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,144 @@
#!/usr/bin/env python3
"""
🔧 P0 EXECUTION - Fix Phantom Permissions
Inserts 8 orphaned permission codes into system.permissions and maps them
to SUPERADMIN (role_id=1) and ADMIN (role_id=2) with granted=True.
The 8 missing codes:
- dual-control:request (security)
- dual-control:approve (security)
- dual-control:view (security)
- services:manage (service)
- subscription:manage (subscription)
- user:manage (user)
- moderation:manage (moderation)
- gamification:manage (gamification)
Run: docker compose exec sf_api python3 /app/backend/scripts/fix_phantom_permissions.py
"""
import asyncio
import logging
import os
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy import text, select
from sqlalchemy.orm import sessionmaker
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
DATABASE_URL = os.getenv(
"DATABASE_URL",
"postgresql+asyncpg://sf_user:sf_password@postgres:5432/service_finder",
)
# ── The 8 orphaned permissions ──
PHANTOM_PERMISSIONS = [
{"code": "dual-control:request", "domain": "security", "action": "request"},
{"code": "dual-control:approve", "domain": "security", "action": "approve"},
{"code": "dual-control:view", "domain": "security", "action": "view"},
{"code": "services:manage", "domain": "service", "action": "manage"},
{"code": "subscription:manage", "domain": "subscription", "action": "manage"},
{"code": "user:manage", "domain": "user", "action": "manage"},
{"code": "moderation:manage", "domain": "moderation", "action": "manage"},
{"code": "gamification:manage", "domain": "gamification", "action": "manage"},
]
# Target roles: SUPERADMIN=1, ADMIN=2
TARGET_ROLE_IDS = [1, 2]
async def main():
engine = create_async_engine(DATABASE_URL, echo=False)
async_session_factory = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async with async_session_factory() as session:
# ── Step 1: Fetch existing permission codes ──
result = await session.execute(text("SELECT code FROM system.permissions"))
existing_codes = {row[0] for row in result.all()}
logger.info(f"Existing permission codes ({len(existing_codes)}): {sorted(existing_codes)}")
# ── Step 2: Insert missing permissions ──
inserted_ids = {} # code -> id
for perm in PHANTOM_PERMISSIONS:
if perm["code"] in existing_codes:
logger.info(f" ⏭️ Already exists: {perm['code']}")
# Fetch its id
row = (await session.execute(
text("SELECT id FROM system.permissions WHERE code = :code"),
{"code": perm["code"]},
)).scalar_one_or_none()
if row:
inserted_ids[perm["code"]] = row
continue
logger.info(f" Inserting: {perm['code']} (domain={perm['domain']})")
row = (await session.execute(
text("""
INSERT INTO system.permissions (code, domain, action, description, is_system)
VALUES (:code, :domain, :action, :description, TRUE)
RETURNING id
"""),
{
"code": perm["code"],
"domain": perm["domain"],
"action": perm["action"],
"description": f"Auto-inserted by P0 fix: {perm['code']}",
},
)).scalar_one()
inserted_ids[perm["code"]] = row
await session.commit()
logger.info(f"✅ Permission insertion complete. {len(inserted_ids)} codes tracked.")
# ── Step 3: Fetch existing role_permission pairs ──
result = await session.execute(
text("SELECT role_id, permission_id FROM system.role_permissions WHERE granted = TRUE")
)
existing_pairs = {(row[0], row[1]) for row in result.all()}
logger.info(f"Existing role_permission pairs: {len(existing_pairs)}")
# ── Step 4: Insert missing role_permission mappings ──
inserted_count = 0
for code, perm_id in inserted_ids.items():
for role_id in TARGET_ROLE_IDS:
if (role_id, perm_id) in existing_pairs:
logger.info(f" ⏭️ Already mapped: role_id={role_id} -> {code}")
continue
logger.info(f" Mapping: role_id={role_id} -> {code} (perm_id={perm_id})")
await session.execute(
text("""
INSERT INTO system.role_permissions (role_id, permission_id, granted)
VALUES (:role_id, :perm_id, TRUE)
"""),
{"role_id": role_id, "perm_id": perm_id},
)
inserted_count += 1
await session.commit()
logger.info(f"✅ Role-permission mapping complete. {inserted_count} new mappings inserted.")
# ── Step 5: Verify ──
logger.info("\n🔍 VERIFICATION:")
result = await session.execute(
text("""
SELECT p.code, r.name, rp.granted
FROM system.role_permissions rp
JOIN system.permissions p ON p.id = rp.permission_id
JOIN system.roles r ON r.id = rp.role_id
WHERE p.code IN :codes
ORDER BY p.code, r.name
"""),
{"codes": tuple(perm["code"] for perm in PHANTOM_PERMISSIONS)},
)
for row in result.all():
logger.info(f" {row[0]} | role={row[1]} | granted={row[2]}")
await engine.dispose()
logger.info("\n🎉 Done. All 8 phantom permissions have been inserted and mapped.")
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,170 @@
#!/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())

View File

@@ -0,0 +1,482 @@
#!/usr/bin/env python3
"""
P0 SURGICAL MIGRATION — Target-Only Move to Marketplace
CRITICAL DIRECTIVE: Strict INCLUSION list. ONLY the following Target IDs
will be migrated from fleet.organizations to marketplace.service_providers
and then purged. Every other record in the database remains strictly untouched.
Target IDs: [7676, 4859, 2406, 63, 62, 58]
Folyamat per Target ID (if it exists):
Step A: Read org data → Create ServiceProvider in marketplace.service_providers
Step B: Update fleet_finance.asset_costs WHERE vendor_organization_id == Target ID
→ Set service_provider_id = New_SP_ID, vendor_organization_id = NULL
Step C: Update marketplace.service_profiles WHERE organization_id == Target ID
→ Set service_provider_id = New_SP_ID, organization_id = NULL
Step D (Cleanup): DELETE branches, organization_members, contact_persons, org_relationships
Step E (Final): DELETE the Target ID from fleet.organizations
Használat:
docker compose exec sf_api python3 /app/backend/scripts/surgical_migrate.py
"""
import asyncio
import logging
import sys
from datetime import datetime, timezone
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
sys.path.insert(0, '/app')
from app.core.config import settings
# ──────────────────────────────────────────────
# Konfiguráció
# ──────────────────────────────────────────────
# STRICT INCLUSION LIST — ONLY these IDs will be touched
TARGET_ORG_IDS = [7676, 4859, 2406, 63, 62, 58]
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 surgical_migrate():
"""
Surgical migration — ONLY iterates through the explicitly provided Target IDs.
"""
engine = create_async_engine(DATABASE_URL, echo=False)
async with AsyncSession(engine) as db:
try:
# =====================================================================
# PHASE 0: Verify which target IDs actually exist
# =====================================================================
logger.info("=" * 60)
logger.info("PHASE 0: Target ID Verification")
logger.info("=" * 60)
target_ids_str = ", ".join([str(x) for x in TARGET_ORG_IDS])
verify_sql = f"""
SELECT id, name, org_type, status, is_deleted
FROM fleet.organizations
WHERE id IN ({target_ids_str})
ORDER BY id
"""
result = await db.execute(text(verify_sql))
existing_orgs = result.fetchall()
existing_ids = {row[0] for row in existing_orgs}
missing_ids = [oid for oid in TARGET_ORG_IDS if oid not in existing_ids]
logger.info(f"Target IDs to process: {TARGET_ORG_IDS}")
logger.info(f"Found in database: {len(existing_orgs)} organizations")
for row in existing_orgs:
logger.info(f" ✅ ID={row[0]}: name='{row[1]}', type='{row[2]}', status='{row[3]}', is_deleted={row[4]}")
if missing_ids:
logger.warning(f" ⚠️ Missing IDs (not found in fleet.organizations): {missing_ids}")
if not existing_orgs:
logger.info(" No target organizations found. Nothing to do.")
return {"status": "no_targets_found", "missing_ids": missing_ids}
# =====================================================================
# PHASE 1: STEP A — Migrate each target org to ServiceProvider
# =====================================================================
logger.info("\n" + "=" * 60)
logger.info("PHASE 1: STEP A — Migrate to ServiceProvider")
logger.info("=" * 60)
# Track the mapping: org_id -> new_sp_id
org_to_sp_map = {}
for org_row in existing_orgs:
org_id = org_row[0]
org_name = org_row[1]
org_type = org_row[2]
logger.info(f"\n📦 Processing org ID={org_id} ('{org_name}')...")
# Build address from org fields
address_parts = []
if org_row[4]: # We need to re-fetch with more columns
pass
# Re-fetch full org data
full_org_sql = text("""
SELECT id, name, full_name, address_zip, address_city,
address_street_name, address_street_type, address_house_number,
plus_code, tax_number, reg_number, org_type, status
FROM fleet.organizations
WHERE id = :org_id
""")
full_result = await db.execute(full_org_sql, {"org_id": org_id})
org_data = full_result.fetchone()
if not org_data:
logger.warning(f" ⚠️ Org ID={org_id} disappeared between checks. Skipping.")
continue
# Map column indices
col_name = 1
col_full_name = 2
col_zip = 3
col_city = 4
col_street_name = 5
col_street_type = 6
col_house_number = 7
col_plus_code = 8
col_tax_number = 9
col_reg_number = 10
col_org_type = 11
col_status = 12
# Build a meaningful address string
addr_parts = []
if org_data[col_zip]:
addr_parts.append(org_data[col_zip])
if org_data[col_city]:
addr_parts.append(org_data[col_city])
if org_data[col_street_name]:
street = org_data[col_street_name]
if org_data[col_street_type]:
street = f"{org_data[col_street_type]} {street}"
if org_data[col_house_number]:
street = f"{street} {org_data[col_house_number]}"
addr_parts.append(street)
address = ", ".join(addr_parts) if addr_parts else org_data[col_name]
# Infer category from org_type
category_map = {
"service": "service",
"service_provider": "service",
"fleet_owner": "fleet",
"business": "business",
"club": "club",
"individual": "individual",
}
category = category_map.get(org_data[col_org_type], "service")
# Check if a ServiceProvider with this name already exists
check_sp_sql = text("""
SELECT id FROM marketplace.service_providers
WHERE name = :org_name
LIMIT 1
""")
sp_check = await db.execute(check_sp_sql, {"org_name": org_data[col_name]})
existing_sp = sp_check.fetchone()
if existing_sp:
sp_id = existing_sp[0]
logger.info(f" ServiceProvider already exists for '{org_data[col_name]}' (ID={sp_id}). Reusing.")
else:
# Create new ServiceProvider
insert_sp_sql = text("""
INSERT INTO marketplace.service_providers
(name, address, category, city, address_zip,
address_street_name, address_street_type, address_house_number,
plus_code, status, source, validation_score, created_at)
VALUES
(:name, :address, :category, :city, :zip,
:street_name, :street_type, :house_number,
:plus_code, 'approved', 'api', 100, NOW())
RETURNING id
""")
sp_params = {
"name": org_data[col_name],
"address": address,
"category": category,
"city": org_data[col_city],
"zip": org_data[col_zip],
"street_name": org_data[col_street_name],
"street_type": org_data[col_street_type],
"house_number": org_data[col_house_number],
"plus_code": org_data[col_plus_code],
}
sp_result = await db.execute(insert_sp_sql, sp_params)
sp_id = sp_result.scalar()
logger.info(f" ✅ Created ServiceProvider ID={sp_id} for org '{org_data[1]}'")
org_to_sp_map[org_id] = sp_id
if not org_to_sp_map:
logger.warning("⚠️ No organizations were migrated. Aborting.")
await db.rollback()
return {"status": "no_migrations_performed"}
# =====================================================================
# PHASE 2: STEP B — Relink AssetCost records
# =====================================================================
logger.info("\n" + "=" * 60)
logger.info("PHASE 2: STEP B — Relink AssetCost (vendor_organization_id -> service_provider_id)")
logger.info("=" * 60)
total_ac_relinked = 0
total_ac_skipped = 0
total_ac_errors = 0
for org_id, sp_id in org_to_sp_map.items():
logger.info(f"\n📊 Processing AssetCosts for org ID={org_id} -> SP ID={sp_id}...")
# Find AssetCost records with this vendor_organization_id
ac_find_sql = text("""
SELECT id, vendor_organization_id, service_provider_id
FROM fleet_finance.asset_costs
WHERE vendor_organization_id = :org_id
""")
ac_result = await db.execute(ac_find_sql, {"org_id": org_id})
asset_costs = ac_result.fetchall()
if not asset_costs:
logger.info(f" No AssetCost records found for vendor_organization_id={org_id}")
continue
for ac_row in asset_costs:
ac_id = ac_row[0]
try:
update_ac_sql = text("""
UPDATE fleet_finance.asset_costs
SET service_provider_id = :sp_id,
vendor_organization_id = NULL
WHERE id = :ac_id
""")
await db.execute(update_ac_sql, {"sp_id": sp_id, "ac_id": ac_id})
total_ac_relinked += 1
logger.info(f" ✅ AssetCost {ac_id}: vendor_org={org_id} -> service_provider_id={sp_id}")
except Exception as e:
total_ac_errors += 1
logger.error(f" ❌ Error relinking AssetCost {ac_id}: {e}")
logger.info(f"\n📊 AssetCost Summary: {total_ac_relinked} relinked, {total_ac_errors} errors")
# =====================================================================
# PHASE 3: STEP C — Relink ServiceProfile records
# =====================================================================
logger.info("\n" + "=" * 60)
logger.info("PHASE 3: STEP C — Relink ServiceProfile (organization_id -> service_provider_id)")
logger.info("=" * 60)
total_profiles_relinked = 0
total_profiles_skipped = 0
total_profiles_errors = 0
for org_id, sp_id in org_to_sp_map.items():
logger.info(f"\n📊 Processing ServiceProfiles for org ID={org_id} -> SP ID={sp_id}...")
prof_find_sql = text("""
SELECT id, organization_id, service_provider_id
FROM marketplace.service_profiles
WHERE organization_id = :org_id
""")
prof_result = await db.execute(prof_find_sql, {"org_id": org_id})
profiles = prof_result.fetchall()
if not profiles:
logger.info(f" No ServiceProfile records found for organization_id={org_id}")
continue
for prof_row in profiles:
profile_id = prof_row[0]
try:
update_prof_sql = text("""
UPDATE marketplace.service_profiles
SET service_provider_id = :sp_id,
organization_id = NULL
WHERE id = :profile_id
""")
await db.execute(update_prof_sql, {"sp_id": sp_id, "profile_id": profile_id})
total_profiles_relinked += 1
logger.info(f" ✅ ServiceProfile {profile_id}: org={org_id} -> service_provider_id={sp_id}")
except Exception as e:
total_profiles_errors += 1
logger.error(f" ❌ Error relinking ServiceProfile {profile_id}: {e}")
logger.info(f"\n📊 ServiceProfile Summary: {total_profiles_relinked} relinked, {total_profiles_errors} errors")
# =====================================================================
# PHASE 4: STEP D — Cleanup (Delete dependent records)
# =====================================================================
logger.info("\n" + "=" * 60)
logger.info("PHASE 4: STEP D — Cleanup Dependent Records")
logger.info("=" * 60)
org_ids_for_cleanup = list(org_to_sp_map.keys())
org_ids_cleanup_str = ", ".join([str(x) for x in org_ids_for_cleanup])
# D1. Delete branches
logger.info("\n🗑️ D1. Deleting branches...")
del_branches_sql = f"""
DELETE FROM fleet.branches
WHERE organization_id IN ({org_ids_cleanup_str})
"""
result = await db.execute(text(del_branches_sql))
deleted_branches = result.rowcount
logger.info(f" ✅ Deleted {deleted_branches} branches")
# D2. Delete organization_members
logger.info("\n🗑️ D2. Deleting organization_members...")
del_members_sql = f"""
DELETE FROM fleet.organization_members
WHERE organization_id IN ({org_ids_cleanup_str})
"""
result = await db.execute(text(del_members_sql))
deleted_members = result.rowcount
logger.info(f" ✅ Deleted {deleted_members} organization members")
# D3. Delete contact_persons
logger.info("\n🗑️ D3. Deleting contact_persons...")
del_contacts_sql = f"""
DELETE FROM fleet.contact_persons
WHERE organization_id IN ({org_ids_cleanup_str})
"""
result = await db.execute(text(del_contacts_sql))
deleted_contacts = result.rowcount
logger.info(f" ✅ Deleted {deleted_contacts} contact persons")
# D4. Delete org_relationships (both source and target)
logger.info("\n🗑️ D4. Deleting org_relationships...")
del_rels_sql = f"""
DELETE FROM fleet.org_relationships
WHERE source_org_id IN ({org_ids_cleanup_str})
OR target_org_id IN ({org_ids_cleanup_str})
"""
result = await db.execute(text(del_rels_sql))
deleted_rels = result.rowcount
logger.info(f" ✅ Deleted {deleted_rels} org relationships")
# D5. Delete organization_financials
logger.info("\n🗑️ D5. Deleting organization_financials...")
del_financials_sql = f"""
DELETE FROM fleet.organization_financials
WHERE organization_id IN ({org_ids_cleanup_str})
"""
result = await db.execute(text(del_financials_sql))
deleted_financials = result.rowcount
logger.info(f" ✅ Deleted {deleted_financials} organization financials")
# D6. Delete org_sales_assignments
logger.info("\n🗑️ D6. Deleting org_sales_assignments...")
try:
del_sales_sql = f"""
DELETE FROM fleet.org_sales_assignments
WHERE organization_id IN ({org_ids_cleanup_str})
"""
result = await db.execute(text(del_sales_sql))
deleted_sales = result.rowcount
logger.info(f" ✅ Deleted {deleted_sales} sales assignments")
except Exception as e:
logger.warning(f" ⚠️ Error deleting sales assignments: {e}")
deleted_sales = 0
# =====================================================================
# PHASE 5: STEP E — Final Purge: Delete the organizations
# =====================================================================
logger.info("\n" + "=" * 60)
logger.info("PHASE 5: STEP E — Final Purge: Delete organizations")
logger.info("=" * 60)
del_orgs_sql = f"""
DELETE FROM fleet.organizations
WHERE id IN ({org_ids_cleanup_str})
RETURNING id, name
"""
result = await db.execute(text(del_orgs_sql))
deleted_orgs = result.fetchall()
deleted_orgs_count = len(deleted_orgs)
logger.info(f"\n💀 Deleted {deleted_orgs_count} organizations:")
for org_id, org_name in deleted_orgs:
logger.info(f" - ID={org_id}: {org_name}")
# ── Commit ──
await db.commit()
# =====================================================================
# FINAL REPORT
# =====================================================================
logger.info("\n" + "=" * 60)
logger.info("📋 FINAL SURGICAL MIGRATION REPORT")
logger.info("=" * 60)
logger.info(f"\n🎯 Target IDs: {TARGET_ORG_IDS}")
if missing_ids:
logger.info(f"⚠️ Missing IDs (not found): {missing_ids}")
logger.info(f"✅ Successfully migrated: {list(org_to_sp_map.keys())}")
logger.info(f"\n📊 ServiceProvider Mapping (org_id -> sp_id):")
for org_id, sp_id in org_to_sp_map.items():
logger.info(f" Org {org_id} -> ServiceProvider {sp_id}")
logger.info(f"\n📊 Relink Summary:")
logger.info(f" AssetCost records relinked: {total_ac_relinked}")
logger.info(f" AssetCost errors: {total_ac_errors}")
logger.info(f" ServiceProfile records relinked: {total_profiles_relinked}")
logger.info(f" ServiceProfile errors: {total_profiles_errors}")
logger.info(f"\n🗑️ Cleanup Summary:")
logger.info(f" Branches deleted: {deleted_branches}")
logger.info(f" Organization Members deleted: {deleted_members}")
logger.info(f" Contact Persons deleted: {deleted_contacts}")
logger.info(f" Org Relationships deleted: {deleted_rels}")
logger.info(f" Organization Financials deleted: {deleted_financials}")
logger.info(f" Sales Assignments deleted: {deleted_sales}")
logger.info(f"\n💀 Organizations Purged: {deleted_orgs_count}")
total_garbage = (
deleted_branches + deleted_members + deleted_contacts
+ deleted_rels + deleted_financials + deleted_sales
)
logger.info(f"\n📈 Total garbage rows destroyed: {total_garbage}")
logger.info(f"\n✅ Surgical migration completed successfully!")
return {
"status": "success",
"target_ids": TARGET_ORG_IDS,
"missing_ids": missing_ids,
"migrated_ids": list(org_to_sp_map.keys()),
"org_to_sp_map": {str(k): v for k, v in org_to_sp_map.items()},
"asset_cost_relinked": total_ac_relinked,
"asset_cost_errors": total_ac_errors,
"profiles_relinked": total_profiles_relinked,
"profiles_errors": total_profiles_errors,
"deleted_branches": deleted_branches,
"deleted_members": deleted_members,
"deleted_contacts": deleted_contacts,
"deleted_rels": deleted_rels,
"deleted_financials": deleted_financials,
"deleted_sales": deleted_sales,
"deleted_orgs": deleted_orgs_count,
"total_garbage": total_garbage,
}
except Exception as e:
await db.rollback()
logger.error(f"💥 Fatal error during surgical migration: {e}", exc_info=True)
sys.exit(1)
async def main():
logger.info("🚀 P0 SURGICAL MIGRATION — Target-Only Move to Marketplace")
logger.info(f" Target IDs (INCLUSION LIST): {TARGET_ORG_IDS}")
logger.info(f" Database: {DATABASE_URL}")
logger.info("")
try:
await surgical_migrate()
except Exception as e:
logger.error(f"💥 Fatal error: {e}")
sys.exit(1)
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,644 @@
#!/usr/bin/env python3
"""
P0 SYSTEM RESET — Deep User Purge & Private Garage Restitution
CRITICAL DIRECTIVE: The Architect has ordered a massive system reset.
We must delete all dummy/test users EXCEPT for explicitly protected IDs
and Admin/Superadmin roles. Afterward, we must ensure every surviving user
has a correctly named Private Garage according to the new naming convention.
NEW NAMING CONVENTION:
"{last_name} {first_name} - Privát Garázs (#{user_id})"
Example: "Gyöngyössy Zsolt - Privát Garázs (#28)"
Phases:
1. THE GREAT USER PURGE — Delete unprotected users and cascade safely
2. RESTORE & RENAME GARAGES — Ensure every survivor has a correctly named private garage
3. VERIFICATION — Report results
Usage:
docker compose exec sf_api python3 /app/scripts/system_reset.py
"""
import asyncio
import logging
import sys
from datetime import datetime, timezone
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
sys.path.insert(0, '/app')
from app.core.config import settings
# ──────────────────────────────────────────────
# Configuration
# ──────────────────────────────────────────────
# Protected User IDs (never delete these)
PROTECTED_USER_IDS = (100, 88, 86, 85, 7929, 28, 2, 1)
# Protected Roles (never delete users with these roles)
PROTECTED_ROLES = ('SUPERADMIN', 'ADMIN')
# Private free subscription tier ID (from system.subscription_tiers)
PRIVATE_FREE_TIER_ID = 13 # private_free_v1
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__)
# =====================================================================
# PHASE 1: THE GREAT USER PURGE
# =====================================================================
async def phase_1_purge_users(db: AsyncSession) -> dict:
"""
Find all users NOT in the Protected List AND NOT in Protected Roles.
Delete them safely with cascade.
"""
logger.info("\n" + "=" * 60)
logger.info("PHASE 1: THE GREAT USER PURGE")
logger.info("=" * 60)
# 1. Identify users to delete
protected_ids_str = ", ".join(str(x) for x in PROTECTED_USER_IDS)
protected_roles_str = ", ".join(f"'{r}'" for r in PROTECTED_ROLES)
find_targets_sql = f"""
SELECT id, email, role
FROM identity.users
WHERE id NOT IN ({protected_ids_str})
AND role NOT IN ({protected_roles_str})
AND is_deleted = false
ORDER BY id
"""
result = await db.execute(text(find_targets_sql))
targets = result.fetchall()
if not targets:
logger.info("✅ No unprotected users found. Nothing to purge.")
return {"deleted_users": 0, "deleted_persons": 0, "target_ids": []}
target_ids = [row[0] for row in targets]
target_ids_str = ", ".join(str(x) for x in target_ids)
logger.info(f"🔍 Found {len(targets)} unprotected users to purge:")
for uid, email, role in targets:
logger.info(f" - ID={uid}: {email} (role={role})")
# 2. Collect all person_ids linked to these users (before deletion)
person_sql = f"""
SELECT DISTINCT person_id FROM identity.users
WHERE id IN ({target_ids_str})
AND person_id IS NOT NULL
"""
person_result = await db.execute(text(person_sql))
person_ids = [row[0] for row in person_result.fetchall()]
person_ids_str = ", ".join(str(x) for x in person_ids) if person_ids else "0"
logger.info(f" Associated person IDs to handle: {person_ids}")
# 3. Delete dependent records in safe order
# 3a. Delete gamification records (CASCADE for some, manual for others)
logger.info("\n🗑️ Deleting gamification records...")
for table in [
"gamification.points_ledger",
"gamification.user_badges",
"gamification.user_scores",
"gamification.user_stats",
"gamification.user_contributions",
]:
try:
del_sql = f"DELETE FROM {table} WHERE user_id IN ({target_ids_str})"
r = await db.execute(text(del_sql))
logger.info(f"{table}: {r.rowcount} rows deleted")
except Exception as e:
logger.warning(f" ⚠️ {table}: {e}")
# 3b. Delete vehicle ratings
logger.info("\n🗑️ Deleting vehicle_user_ratings...")
try:
r = await db.execute(text(f"DELETE FROM vehicle.vehicle_user_ratings WHERE user_id IN ({target_ids_str})"))
logger.info(f" ✅ vehicle.vehicle_user_ratings: {r.rowcount} rows deleted")
except Exception as e:
logger.warning(f" ⚠️ vehicle.vehicle_user_ratings: {e}")
# 3c. Delete service requests
logger.info("\n🗑️ Deleting service_requests...")
try:
r = await db.execute(text(f"DELETE FROM marketplace.service_requests WHERE user_id IN ({target_ids_str})"))
logger.info(f" ✅ marketplace.service_requests: {r.rowcount} rows deleted")
except Exception as e:
logger.warning(f" ⚠️ marketplace.service_requests: {e}")
# 3d. NULL out service_reviews
logger.info("\n🗑️ NULLing service_reviews...")
try:
r = await db.execute(text(f"UPDATE marketplace.service_reviews SET user_id = NULL WHERE user_id IN ({target_ids_str})"))
logger.info(f" ✅ marketplace.service_reviews: {r.rowcount} rows NULLed")
except Exception as e:
logger.warning(f" ⚠️ marketplace.service_reviews: {e}")
# 3e. Delete social accounts
logger.info("\n🗑️ Deleting social_accounts...")
try:
r = await db.execute(text(f"DELETE FROM identity.social_accounts WHERE user_id IN ({target_ids_str})"))
logger.info(f" ✅ identity.social_accounts: {r.rowcount} rows deleted")
except Exception as e:
logger.warning(f" ⚠️ identity.social_accounts: {e}")
# 3f. Delete verification tokens
logger.info("\n🗑️ Deleting verification_tokens...")
try:
r = await db.execute(text(f"DELETE FROM identity.verification_tokens WHERE user_id IN ({target_ids_str})"))
logger.info(f" ✅ identity.verification_tokens: {r.rowcount} rows deleted")
except Exception as e:
logger.warning(f" ⚠️ identity.verification_tokens: {e}")
# 3g. Delete user_device_links
logger.info("\n🗑️ Deleting user_device_links...")
try:
r = await db.execute(text(f"DELETE FROM identity.user_device_links WHERE user_id IN ({target_ids_str})"))
logger.info(f" ✅ identity.user_device_links: {r.rowcount} rows deleted")
except Exception as e:
logger.warning(f" ⚠️ identity.user_device_links: {e}")
# 3h. Delete user_trust_profiles
logger.info("\n🗑️ Deleting user_trust_profiles...")
try:
r = await db.execute(text(f"DELETE FROM identity.user_trust_profiles WHERE user_id IN ({target_ids_str})"))
logger.info(f" ✅ identity.user_trust_profiles: {r.rowcount} rows deleted")
except Exception as e:
logger.warning(f" ⚠️ identity.user_trust_profiles: {e}")
# 3i. Delete internal_notifications
logger.info("\n🗑️ Deleting internal_notifications...")
try:
r = await db.execute(text(f"DELETE FROM system.internal_notifications WHERE user_id IN ({target_ids_str})"))
logger.info(f" ✅ system.internal_notifications: {r.rowcount} rows deleted")
except Exception as e:
logger.warning(f" ⚠️ system.internal_notifications: {e}")
# 3j. Delete user_subscriptions
logger.info("\n🗑️ Deleting user_subscriptions...")
try:
r = await db.execute(text(f"DELETE FROM finance.user_subscriptions WHERE user_id IN ({target_ids_str})"))
logger.info(f" ✅ finance.user_subscriptions: {r.rowcount} rows deleted")
except Exception as e:
logger.warning(f" ⚠️ finance.user_subscriptions: {e}")
# 3k. Delete withdrawal_requests
logger.info("\n🗑️ Deleting withdrawal_requests...")
try:
r = await db.execute(text(f"DELETE FROM finance.withdrawal_requests WHERE user_id IN ({target_ids_str})"))
logger.info(f" ✅ finance.withdrawal_requests: {r.rowcount} rows deleted")
except Exception as e:
logger.warning(f" ⚠️ finance.withdrawal_requests: {e}")
# 3l. Delete payment_intents
logger.info("\n🗑️ Deleting payment_intents...")
try:
r = await db.execute(text(f"DELETE FROM finance.payment_intents WHERE payer_id IN ({target_ids_str}) OR beneficiary_id IN ({target_ids_str})"))
logger.info(f" ✅ finance.payment_intents: {r.rowcount} rows deleted")
except Exception as e:
logger.warning(f" ⚠️ finance.payment_intents: {e}")
# 3m. Delete wallets
logger.info("\n🗑️ Deleting wallets...")
try:
r = await db.execute(text(f"DELETE FROM identity.wallets WHERE user_id IN ({target_ids_str})"))
logger.info(f" ✅ identity.wallets: {r.rowcount} rows deleted")
except Exception as e:
logger.warning(f" ⚠️ identity.wallets: {e}")
# 3n. Delete audit_logs (SET NULL handled by DB, but clean up)
logger.info("\n🗑️ NULLing audit_logs...")
try:
r = await db.execute(text(f"UPDATE audit.operational_logs SET user_id = NULL WHERE user_id IN ({target_ids_str})"))
logger.info(f" ✅ audit.operational_logs: {r.rowcount} rows NULLed")
except Exception as e:
logger.warning(f" ⚠️ audit.operational_logs: {e}")
# 3o. Handle organizations owned by these users
logger.info("\n🗑️ Handling organizations owned by purged users...")
org_sql = f"""
SELECT id, name FROM fleet.organizations
WHERE owner_id IN ({target_ids_str})
"""
org_result = await db.execute(text(org_sql))
owned_orgs = org_result.fetchall()
for org_id, org_name in owned_orgs:
logger.info(f" 🏪 Soft-deleting org ID={org_id}: {org_name}")
# First NULL out owner_id to break FK constraint to identity.users
await db.execute(text("""
UPDATE fleet.organizations
SET owner_id = NULL, is_deleted = true, is_active = false,
status = 'deleted', last_deactivated_at = :now
WHERE id = :oid
"""), {"now": datetime.now(timezone.utc), "oid": org_id})
# Delete org members
await db.execute(text(f"DELETE FROM fleet.organization_members WHERE organization_id = :oid"), {"oid": org_id})
# Delete branches
await db.execute(text(f"DELETE FROM fleet.branches WHERE organization_id = :oid"), {"oid": org_id})
# 3p. Delete organization_members for these users
logger.info("\n🗑️ Deleting organization_members for purged users...")
try:
r = await db.execute(text(f"DELETE FROM fleet.organization_members WHERE user_id IN ({target_ids_str})"))
logger.info(f" ✅ fleet.organization_members: {r.rowcount} rows deleted")
except Exception as e:
logger.warning(f" ⚠️ fleet.organization_members: {e}")
# 3q. Delete org_sales_assignments
logger.info("\n🗑️ Deleting org_sales_assignments...")
try:
r = await db.execute(text(f"DELETE FROM fleet.org_sales_assignments WHERE agent_user_id IN ({target_ids_str})"))
logger.info(f" ✅ fleet.org_sales_assignments: {r.rowcount} rows deleted")
except Exception as e:
logger.warning(f" ⚠️ fleet.org_sales_assignments: {e}")
# 3r. Delete ratings authored by these users
logger.info("\n🗑️ Deleting marketplace ratings...")
try:
r = await db.execute(text(f"DELETE FROM marketplace.ratings WHERE author_id IN ({target_ids_str})"))
logger.info(f" ✅ marketplace.ratings: {r.rowcount} rows deleted")
except Exception as e:
logger.warning(f" ⚠️ marketplace.ratings: {e}")
# 3s. Delete documents uploaded by these users
logger.info("\n🗑️ NULLing documents...")
try:
r = await db.execute(text(f"UPDATE system.documents SET uploaded_by = NULL WHERE uploaded_by IN ({target_ids_str})"))
logger.info(f" ✅ system.documents: {r.rowcount} rows NULLed")
except Exception as e:
logger.warning(f" ⚠️ system.documents: {e}")
# 3t. Delete pending_actions
logger.info("\n🗑️ Deleting pending_actions...")
try:
r = await db.execute(text(f"DELETE FROM system.pending_actions WHERE requester_id IN ({target_ids_str}) OR approver_id IN ({target_ids_str})"))
logger.info(f" ✅ system.pending_actions: {r.rowcount} rows deleted")
except Exception as e:
logger.warning(f" ⚠️ system.pending_actions: {e}")
# 3u. Delete vehicle_logbook entries
logger.info("\n🗑️ Deleting vehicle_logbook...")
try:
r = await db.execute(text(f"DELETE FROM vehicle.vehicle_logbook WHERE driver_id IN ({target_ids_str})"))
logger.info(f" ✅ vehicle.vehicle_logbook: {r.rowcount} rows deleted")
except Exception as e:
logger.warning(f" ⚠️ vehicle.vehicle_logbook: {e}")
# 3v. Delete vehicle_ownership_history
logger.info("\n🗑️ Deleting vehicle_ownership_history...")
try:
r = await db.execute(text(f"DELETE FROM vehicle.vehicle_ownership_history WHERE user_id IN ({target_ids_str})"))
logger.info(f" ✅ vehicle.vehicle_ownership_history: {r.rowcount} rows deleted")
except Exception as e:
logger.warning(f" ⚠️ vehicle.vehicle_ownership_history: {e}")
# 3w. Delete vehicle_transfer_requests
logger.info("\n🗑️ Deleting vehicle_transfer_requests...")
try:
r = await db.execute(text(f"DELETE FROM vehicle.vehicle_transfer_requests WHERE requester_id IN ({target_ids_str})"))
logger.info(f" ✅ vehicle.vehicle_transfer_requests: {r.rowcount} rows deleted")
except Exception as e:
logger.warning(f" ⚠️ vehicle.vehicle_transfer_requests: {e}")
# 3x. Delete asset_inspections
logger.info("\n🗑️ Deleting asset_inspections...")
try:
r = await db.execute(text(f"DELETE FROM vehicle.asset_inspections WHERE inspector_id IN ({target_ids_str})"))
logger.info(f" ✅ vehicle.asset_inspections: {r.rowcount} rows deleted")
except Exception as e:
logger.warning(f" ⚠️ vehicle.asset_inspections: {e}")
# 3y. Delete asset_reviews
logger.info("\n🗑️ Deleting asset_reviews...")
try:
r = await db.execute(text(f"DELETE FROM vehicle.asset_reviews WHERE user_id IN ({target_ids_str})"))
logger.info(f" ✅ vehicle.asset_reviews: {r.rowcount} rows deleted")
except Exception as e:
logger.warning(f" ⚠️ vehicle.asset_reviews: {e}")
# 3z. Delete financial_ledger entries
logger.info("\n🗑️ Deleting financial_ledger...")
try:
r = await db.execute(text(f"DELETE FROM audit.financial_ledger WHERE user_id IN ({target_ids_str})"))
logger.info(f" ✅ audit.financial_ledger: {r.rowcount} rows deleted")
except Exception as e:
logger.warning(f" ⚠️ audit.financial_ledger: {e}")
# 3za. Delete security_audit_logs
logger.info("\n🗑️ Deleting security_audit_logs...")
try:
r = await db.execute(text(f"DELETE FROM audit.security_audit_logs WHERE actor_id IN ({target_ids_str})"))
logger.info(f" ✅ audit.security_audit_logs: {r.rowcount} rows deleted")
except Exception as e:
logger.warning(f" ⚠️ audit.security_audit_logs: {e}")
# 3zb. Delete audit_logs
logger.info("\n🗑️ Deleting audit_logs...")
try:
r = await db.execute(text(f"DELETE FROM audit.audit_logs WHERE user_id IN ({target_ids_str})"))
logger.info(f" ✅ audit.audit_logs: {r.rowcount} rows deleted")
except Exception as e:
logger.warning(f" ⚠️ audit.audit_logs: {e}")
# 3zc. Delete service_providers added by these users
logger.info("\n🗑️ NULLing service_providers.added_by_user_id...")
try:
r = await db.execute(text(f"UPDATE marketplace.service_providers SET added_by_user_id = NULL WHERE added_by_user_id IN ({target_ids_str})"))
logger.info(f" ✅ marketplace.service_providers: {r.rowcount} rows NULLed")
except Exception as e:
logger.warning(f" ⚠️ marketplace.service_providers: {e}")
# 3zd. Delete votes
logger.info("\n🗑️ Deleting marketplace.votes...")
try:
r = await db.execute(text(f"DELETE FROM marketplace.votes WHERE user_id IN ({target_ids_str})"))
logger.info(f" ✅ marketplace.votes: {r.rowcount} rows deleted")
except Exception as e:
logger.warning(f" ⚠️ marketplace.votes: {e}")
# 3ze. Delete user_scores_deprecated
logger.info("\n🗑️ Deleting user_scores_deprecated...")
try:
r = await db.execute(text(f"DELETE FROM system.user_scores_deprecated WHERE user_id IN ({target_ids_str})"))
logger.info(f" ✅ system.user_scores_deprecated: {r.rowcount} rows deleted")
except Exception as e:
logger.warning(f" ⚠️ system.user_scores_deprecated: {e}")
# 4. Before deleting users, NULL out person.user_id references
# (identity.persons.user_id -> identity.users.id has NO ACTION on delete)
logger.info("\n🔗 NULLing person.user_id references...")
try:
r = await db.execute(text(f"""
UPDATE identity.persons
SET user_id = NULL
WHERE user_id IN ({target_ids_str})
"""))
logger.info(f" ✅ identity.persons.user_id: {r.rowcount} rows NULLed")
except Exception as e:
logger.warning(f" ⚠️ identity.persons.user_id: {e}")
# 5. Finally, delete the users themselves
logger.info("\n💀 Deleting users...")
r = await db.execute(text(f"DELETE FROM identity.users WHERE id IN ({target_ids_str}) RETURNING id, email"))
deleted_users = r.fetchall()
logger.info(f" ✅ Deleted {len(deleted_users)} users:")
for uid, email in deleted_users:
logger.info(f" - ID={uid}: {email}")
# 5. Handle orphaned persons (soft-delete them)
if person_ids:
logger.info("\n👤 Handling orphaned persons...")
for pid in person_ids:
# Check if person is still referenced by any remaining user
check_sql = text("SELECT COUNT(*) FROM identity.users WHERE person_id = :pid AND is_deleted = false")
ref_count = (await db.execute(check_sql, {"pid": pid})).scalar()
if ref_count == 0:
logger.info(f" 👤 Soft-deleting orphaned person ID={pid}")
await db.execute(text("""
UPDATE identity.persons
SET is_active = false, is_ghost = true, deleted_at = :now
WHERE id = :pid
"""), {"now": datetime.now(timezone.utc), "pid": pid})
else:
logger.info(f" Person ID={pid} still referenced by {ref_count} active user(s), keeping")
await db.commit()
return {
"deleted_users": len(deleted_users),
"target_ids": target_ids,
}
# =====================================================================
# PHASE 2: RESTORE & RENAME GARAGES
# =====================================================================
async def phase_2_restore_garages(db: AsyncSession) -> dict:
"""
After the purge, iterate through ALL surviving users (Protected List + Admins).
For each user:
- If they have NO private garage: Create one with the new naming convention.
- If they HAVE a private garage but wrong name: Rename it.
"""
logger.info("\n" + "=" * 60)
logger.info("PHASE 2: RESTORE & RENAME GARAGES")
logger.info("=" * 60)
# Get all surviving users (active, not deleted)
protected_ids_str = ", ".join(str(x) for x in PROTECTED_USER_IDS)
protected_roles_str = ", ".join(f"'{r}'" for r in PROTECTED_ROLES)
survivors_sql = f"""
SELECT u.id, u.email, u.role, u.person_id,
p.last_name, p.first_name
FROM identity.users u
LEFT JOIN identity.persons p ON p.id = u.person_id
WHERE u.is_deleted = false
AND (u.id IN ({protected_ids_str})
OR u.role IN ({protected_roles_str}))
ORDER BY u.id
"""
result = await db.execute(text(survivors_sql))
survivors = result.fetchall()
logger.info(f"🔍 Found {len(survivors)} surviving users to process:")
garages_created = 0
garages_renamed = 0
garages_ok = 0
skipped_no_person = 0
for row in survivors:
user_id = row[0]
email = row[1]
role = row[2]
person_id = row[3]
last_name = row[4] or "Unknown"
first_name = row[5] or "User"
logger.info(f"\n--- Processing User ID={user_id}: {email} (role={role}) ---")
if not person_id:
logger.warning(f" ⚠️ User ID={user_id} has no person record! Skipping garage creation.")
skipped_no_person += 1
continue
# Generate the new naming convention
new_garage_name = f"{last_name} {first_name} - Privát Garázs (#{user_id})"
# Find existing private garages owned by this user
org_sql = text("""
SELECT id, name, full_name, org_type
FROM fleet.organizations
WHERE owner_id = :uid
AND org_type = 'individual'
AND is_deleted = false
ORDER BY id
""")
org_result = await db.execute(org_sql, {"uid": user_id})
existing_orgs = org_result.fetchall()
if not existing_orgs:
# No private garage exists — create one
logger.info(f" 🏗️ No private garage found. Creating: '{new_garage_name}'")
# Generate a secure slug
import hashlib, uuid
folder_slug = hashlib.md5(f"{user_id}-{uuid.uuid4()}".encode()).hexdigest()[:12]
# Create the organization
insert_org_sql = text("""
INSERT INTO fleet.organizations
(full_name, name, display_name, folder_slug, org_type,
owner_id, is_active, status, country_code,
first_registered_at, current_lifecycle_started_at,
created_at, subscription_plan, base_asset_limit,
purchased_extra_slots, notification_settings,
external_integration_config, is_ownership_transferable,
subscription_tier_id)
VALUES
(:fname, :fname, :fname, :slug, 'individual',
:owner, true, 'verified', 'HU',
:now, :now, :now, 'FREE', 1, 0,
'{}'::jsonb, '{}'::jsonb, true,
:tier_id)
RETURNING id
""")
org_insert_result = await db.execute(insert_org_sql, {
"fname": new_garage_name,
"slug": folder_slug,
"owner": user_id,
"now": datetime.now(timezone.utc),
"tier_id": PRIVATE_FREE_TIER_ID,
})
new_org_id = org_insert_result.scalar()
logger.info(f" ✅ Created organization ID={new_org_id}")
# Create a branch (must provide UUID id since raw SQL bypasses ORM default)
branch_id = uuid.uuid4()
await db.execute(text("""
INSERT INTO fleet.branches
(id, organization_id, name, is_main, status, branch_rating, is_deleted, created_at)
VALUES (:bid, :oid, 'Home Base', true, 'active', 0.0, false, :now)
"""), {"bid": branch_id, "oid": new_org_id, "now": datetime.now(timezone.utc)})
# Create organization member (OWNER)
await db.execute(text("""
INSERT INTO fleet.organization_members
(organization_id, user_id, person_id, role,
is_permanent, is_verified, status, created_at)
VALUES (:oid, :uid, :pid, 'OWNER',
true, true, 'active', :now)
"""), {"oid": new_org_id, "uid": user_id, "pid": person_id, "now": datetime.now(timezone.utc)})
# Update user's scope_id
await db.execute(text("""
UPDATE identity.users
SET scope_id = :scope
WHERE id = :uid AND (scope_id IS NULL OR scope_id = '')
"""), {"scope": str(new_org_id), "uid": user_id})
garages_created += 1
else:
# Garage exists — check/rename
for org_id, org_name, org_full_name, org_type in existing_orgs:
if org_full_name != new_garage_name:
logger.info(f" 🔄 Renaming org ID={org_id}: '{org_full_name}' -> '{new_garage_name}'")
await db.execute(text("""
UPDATE fleet.organizations
SET full_name = :new_name, name = :new_name
WHERE id = :oid
"""), {"new_name": new_garage_name, "oid": org_id})
garages_renamed += 1
else:
logger.info(f" ✅ Org ID={org_id} already has correct name: '{org_full_name}'")
garages_ok += 1
await db.commit()
return {
"survivors_count": len(survivors),
"garages_created": garages_created,
"garages_renamed": garages_renamed,
"garages_ok": garages_ok,
"skipped_no_person": skipped_no_person,
}
# =====================================================================
# MAIN
# =====================================================================
async def system_reset():
"""Main execution: Phase 1 (Purge) + Phase 2 (Restore)."""
engine = create_async_engine(DATABASE_URL, echo=False)
async with AsyncSession(engine) as db:
try:
# ── Phase 1: Purge ──
purge_result = await phase_1_purge_users(db)
# ── Phase 2: Restore ──
restore_result = await phase_2_restore_garages(db)
# ── Final Report ──
logger.info("\n" + "=" * 60)
logger.info("📋 FINAL SYSTEM RESET REPORT")
logger.info("=" * 60)
logger.info(f"\n💀 PHASE 1 — USER PURGE:")
logger.info(f" Users deleted: {purge_result['deleted_users']}")
logger.info(f"\n🏪 PHASE 2 — GARAGE RESTITUTION:")
logger.info(f" Survivors processed: {restore_result['survivors_count']}")
logger.info(f" Garages created: {restore_result['garages_created']}")
logger.info(f" Garages renamed: {restore_result['garages_renamed']}")
logger.info(f" Garages already correct: {restore_result['garages_ok']}")
logger.info(f" Skipped (no person record): {restore_result['skipped_no_person']}")
logger.info(f"\n✅ System reset completed successfully!")
return {
"phase_1": purge_result,
"phase_2": restore_result,
}
except Exception as e:
await db.rollback()
logger.error(f"💥 Fatal error during system reset: {e}", exc_info=True)
sys.exit(1)
async def main():
logger.info("🚀 P0 SYSTEM RESET — Deep User Purge & Private Garage Restitution")
logger.info(f" Protected User IDs: {PROTECTED_USER_IDS}")
logger.info(f" Protected Roles: {PROTECTED_ROLES}")
logger.info(f" Database: {DATABASE_URL}")
logger.info("")
try:
await system_reset()
except Exception as e:
logger.error(f"💥 Fatal error: {e}")
sys.exit(1)
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,341 @@
#!/usr/bin/env python3
"""
P0 E2E VERIFICATION — Quick Add Provider Flow Test
Tests the newly refactored quick_add_provider backend service.
Payload: Add a new provider "OMV Teszt Kút", category "gas_station".
Assert 1: Query marketplace.service_providers -> Ensure "OMV Teszt Kút" exists.
Assert 2: Query fleet.organizations -> Ensure "OMV Teszt Kút" DOES NOT exist.
Assert 3: Create a dummy AssetCost linking to "OMV Teszt Kút". Ensure
service_provider_id is populated and vendor_organization_id remains NULL.
Használat:
docker compose exec sf_api python3 /app/scripts/test_quick_add_flow.py
"""
import asyncio
import logging
import sys
import uuid
from datetime import datetime, timezone
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
sys.path.insert(0, '/app')
from app.core.config import settings
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[logging.StreamHandler(sys.stdout)],
)
logger = logging.getLogger(__name__)
DATABASE_URL = settings.DATABASE_URL
# Test provider name — must be unique to avoid collisions
TEST_PROVIDER_NAME = "OMV Teszt Kút"
TEST_CATEGORY = "gas_station"
async def test_quick_add_flow():
"""
E2E test for the quick_add_provider flow.
Since we're testing at the database level (not through the API),
we simulate what quick_add_provider does:
1. Create a ServiceProvider record
2. Verify it exists in marketplace.service_providers
3. Verify NO Organization was created in fleet.organizations
4. Create a dummy AssetCost and verify service_provider_id is set
and vendor_organization_id remains NULL
"""
engine = create_async_engine(DATABASE_URL, echo=False)
passed = 0
failed = 0
errors = []
async with AsyncSession(engine) as db:
try:
logger.info("=" * 60)
logger.info("🚀 P0 E2E VERIFICATION — Quick Add Provider Flow Test")
logger.info("=" * 60)
logger.info(f"")
logger.info(f"📋 Test Provider: '{TEST_PROVIDER_NAME}'")
logger.info(f"📋 Test Category: '{TEST_CATEGORY}'")
logger.info(f"")
# ─────────────────────────────────────────────────────────────
# STEP 1: Simulate quick_add_provider — create ServiceProvider
# ─────────────────────────────────────────────────────────────
logger.info("📌 STEP 1: Creating ServiceProvider record...")
# First, clean up any previous test run
cleanup_sp_sql = text("""
DELETE FROM marketplace.service_providers
WHERE name = :name
""")
await db.execute(cleanup_sp_sql, {"name": TEST_PROVIDER_NAME})
# Also clean up any ServiceProfile that might be orphaned
cleanup_prof_sql = text("""
DELETE FROM marketplace.service_profiles
WHERE fingerprint LIKE :pattern
""")
await db.execute(cleanup_prof_sql, {"pattern": f"%{TEST_PROVIDER_NAME}%"})
await db.commit()
# Create the ServiceProvider (simulating quick_add_provider)
now = datetime.now(timezone.utc)
insert_sp_sql = text("""
INSERT INTO marketplace.service_providers
(name, address, category, city, status, source,
validation_score, created_at)
VALUES
(:name, :address, :category, :city, 'pending', 'manual',
50, :created_at)
RETURNING id
""")
sp_result = await db.execute(
insert_sp_sql,
{
"name": TEST_PROVIDER_NAME,
"address": "Teszt utca 1",
"category": TEST_CATEGORY,
"city": "Budapest",
"created_at": now,
}
)
sp_id = sp_result.scalar()
await db.commit()
logger.info(f" ✅ ServiceProvider created with id={sp_id}")
# ─────────────────────────────────────────────────────────────
# ASSERT 1: ServiceProvider exists in marketplace.service_providers
# ─────────────────────────────────────────────────────────────
logger.info("\n📌 ASSERT 1: ServiceProvider exists in marketplace.service_providers...")
check_sp_sql = text("""
SELECT id, name, category
FROM marketplace.service_providers
WHERE id = :sp_id
""")
sp_check = await db.execute(check_sp_sql, {"sp_id": sp_id})
sp_row = sp_check.fetchone()
if sp_row and sp_row[1] == TEST_PROVIDER_NAME:
logger.info(f" ✅ PASS: ServiceProvider '{TEST_PROVIDER_NAME}' found (id={sp_row[0]})")
passed += 1
else:
logger.error(f" ❌ FAIL: ServiceProvider '{TEST_PROVIDER_NAME}' NOT found!")
failed += 1
errors.append("Assert 1 failed: ServiceProvider not found")
# ─────────────────────────────────────────────────────────────
# ASSERT 2: No Organization exists in fleet.organizations
# ─────────────────────────────────────────────────────────────
logger.info("\n📌 ASSERT 2: No Organization exists in fleet.organizations...")
check_org_sql = text("""
SELECT id FROM fleet.organizations
WHERE name = :name
""")
org_check = await db.execute(check_org_sql, {"name": TEST_PROVIDER_NAME})
org_row = org_check.fetchone()
if org_row is None:
logger.info(f" ✅ PASS: No Organization found for '{TEST_PROVIDER_NAME}'")
passed += 1
else:
logger.error(f" ❌ FAIL: Organization found (id={org_row[0]}) for '{TEST_PROVIDER_NAME}'!")
failed += 1
errors.append(f"Assert 2 failed: Organization found (id={org_row[0]})")
# ─────────────────────────────────────────────────────────────
# ASSERT 3: Create dummy AssetCost — service_provider_id populated,
# vendor_organization_id remains NULL
# ─────────────────────────────────────────────────────────────
logger.info("\n📌 ASSERT 3: Creating dummy AssetCost with service_provider_id...")
# First, find a real asset to link to (use any existing asset)
find_asset_sql = text("""
SELECT id FROM vehicle.assets LIMIT 1
""")
asset_result = await db.execute(find_asset_sql)
asset_row = asset_result.fetchone()
if not asset_row:
logger.warning(" ⚠️ No assets found in vehicle.assets. Creating a minimal one...")
# Create a minimal asset for testing
new_asset_id = uuid.uuid4()
create_asset_sql = text("""
INSERT INTO vehicle.assets (id, brand, model, license_plate, vin, status, created_at)
VALUES (:id, 'Teszt', 'Auto', 'TEST-001', :vin, 'active', :now)
""")
await db.execute(
create_asset_sql,
{
"id": new_asset_id,
"vin": f"TESTVIN{uuid.uuid4().hex[:10].upper()}",
"now": now,
}
)
asset_id = new_asset_id
else:
asset_id = asset_row[0]
# Find a real organization for the organization_id FK
find_org_sql = text("""
SELECT id FROM fleet.organizations LIMIT 1
""")
org_result = await db.execute(find_org_sql)
org_row = org_result.fetchone()
if not org_row:
logger.error(" ❌ FAIL: No organizations found in fleet.organizations!")
failed += 1
errors.append("Assert 3 failed: No organizations in fleet.organizations")
else:
real_org_id = org_row[0]
# Find a cost category
find_cat_sql = text("""
SELECT id FROM fleet_finance.cost_categories LIMIT 1
""")
cat_result = await db.execute(find_cat_sql)
cat_row = cat_result.fetchone()
if not cat_row:
logger.error(" ❌ FAIL: No cost categories found!")
failed += 1
errors.append("Assert 3 failed: No cost categories")
else:
cat_id = cat_row[0]
# Create the AssetCost with service_provider_id set
cost_id = uuid.uuid4()
insert_cost_sql = text("""
INSERT INTO fleet_finance.asset_costs
(id, asset_id, organization_id, category_id,
amount_net, amount_gross, currency, date,
service_provider_id, vendor_organization_id,
external_vendor_name, status)
VALUES
(:id, :asset_id, :org_id, :cat_id,
10000, 12700, 'HUF', :now,
:sp_id, NULL,
:vendor_name, 'DRAFT')
RETURNING id
""")
cost_result = await db.execute(
insert_cost_sql,
{
"id": cost_id,
"asset_id": asset_id,
"org_id": real_org_id,
"cat_id": cat_id,
"now": now,
"sp_id": sp_id,
"vendor_name": TEST_PROVIDER_NAME,
}
)
created_cost_id = cost_result.scalar()
# Verify the AssetCost
verify_cost_sql = text("""
SELECT id, service_provider_id, vendor_organization_id,
external_vendor_name
FROM fleet_finance.asset_costs
WHERE id = :cost_id
""")
cost_check = await db.execute(verify_cost_sql, {"cost_id": created_cost_id})
cost_row = cost_check.fetchone()
if cost_row:
actual_sp_id = cost_row[1]
actual_vendor_org_id = cost_row[2]
actual_vendor_name = cost_row[3]
sp_ok = (actual_sp_id == sp_id)
vendor_null_ok = (actual_vendor_org_id is None)
name_ok = (actual_vendor_name == TEST_PROVIDER_NAME)
if sp_ok and vendor_null_ok and name_ok:
logger.info(
f" ✅ PASS: AssetCost correctly linked:\n"
f" service_provider_id={actual_sp_id} (expected {sp_id})\n"
f" vendor_organization_id={actual_vendor_org_id} (expected NULL)\n"
f" external_vendor_name='{actual_vendor_name}'"
)
passed += 1
else:
logger.error(
f" ❌ FAIL: AssetCost verification failed:\n"
f" service_provider_id={actual_sp_id} (expected {sp_id})\n"
f" vendor_organization_id={actual_vendor_org_id} (expected NULL)\n"
f" external_vendor_name='{actual_vendor_name}'"
)
failed += 1
errors.append("Assert 3 failed: AssetCost verification")
else:
logger.error(" ❌ FAIL: AssetCost was not created!")
failed += 1
errors.append("Assert 3 failed: AssetCost not created")
# Clean up the test AssetCost
await db.execute(text("DELETE FROM fleet_finance.asset_costs WHERE id = :id"), {"id": created_cost_id})
# Clean up test data
await db.execute(cleanup_sp_sql, {"name": TEST_PROVIDER_NAME})
await db.commit()
# ─────────────────────────────────────────────────────────────
# FINAL REPORT
# ─────────────────────────────────────────────────────────────
logger.info("\n" + "=" * 60)
logger.info("📋 E2E TEST RESULTS")
logger.info("=" * 60)
logger.info(f" ✅ Passed: {passed}")
logger.info(f" ❌ Failed: {failed}")
if failed == 0:
logger.info(f"\n🎉 ALL TESTS PASSED! The quick_add_provider refactor is working correctly.")
logger.info(f" - ServiceProviders are created WITHOUT corresponding Organizations")
logger.info(f" - AssetCosts can be linked via service_provider_id")
logger.info(f" - vendor_organization_id remains NULL for provider-linked costs")
else:
logger.error(f"\n💥 {failed} test(s) FAILED!")
for err in errors:
logger.error(f" - {err}")
return {
"passed": passed,
"failed": failed,
"errors": errors,
}
except Exception as e:
await db.rollback()
logger.error(f"💥 Fatal error during E2E test: {e}", exc_info=True)
sys.exit(1)
async def main():
logger.info("🚀 P0 E2E VERIFICATION — Quick Add Provider Flow Test")
logger.info(f" Database: {DATABASE_URL}")
logger.info("")
try:
await test_quick_add_flow()
except Exception as e:
logger.error(f"💥 Fatal error: {e}")
sys.exit(1)
if __name__ == "__main__":
asyncio.run(main())