admin felület fejlesztése garázs, előfizeztési csomagok
This commit is contained in:
190
backend/scripts/migrate_providers.py
Normal file
190
backend/scripts/migrate_providers.py
Normal file
@@ -0,0 +1,190 @@
|
||||
#!/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())
|
||||
373
backend/scripts/seed_regions.py
Normal file
373
backend/scripts/seed_regions.py
Normal file
@@ -0,0 +1,373 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
🌍 Seed script for Global Region Registry (system.region_config).
|
||||
|
||||
Populates the system.region_config table with ALL 27 EU member states,
|
||||
plus United Kingdom (GB), Switzerland (CH), and a DEFAULT/Eurozone fallback.
|
||||
|
||||
Total: 30 regions with accurate VAT rates, currencies, locales, and timezones.
|
||||
|
||||
Usage:
|
||||
docker compose exec sf_api python3 /app/scripts/seed_regions.py
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Ensure we can import from the project
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||
|
||||
from app.core.config import settings
|
||||
from app.models.core_logic import RegionConfig
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# 🌍 COMPREHENSIVE EU + EEA REGION REGISTRY
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# All 27 EU member states + GB + CH + DEFAULT fallback = 30 entries
|
||||
# VAT rates verified against official EU VAT rates (2025-2026)
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
DEFAULT_REGIONS = [
|
||||
# ── EU MEMBER STATES (27) ──────────────────────────────────────────────
|
||||
{
|
||||
"country_code": "AT",
|
||||
"name": "Austria",
|
||||
"currency": "EUR",
|
||||
"default_vat_rate": 20.0,
|
||||
"locale_code": "de-AT",
|
||||
"timezone": "Europe/Vienna",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"country_code": "BE",
|
||||
"name": "Belgium",
|
||||
"currency": "EUR",
|
||||
"default_vat_rate": 21.0,
|
||||
"locale_code": "nl-BE",
|
||||
"timezone": "Europe/Brussels",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"country_code": "BG",
|
||||
"name": "Bulgaria",
|
||||
"currency": "BGN",
|
||||
"default_vat_rate": 20.0,
|
||||
"locale_code": "bg-BG",
|
||||
"timezone": "Europe/Sofia",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"country_code": "HR",
|
||||
"name": "Croatia",
|
||||
"currency": "EUR",
|
||||
"default_vat_rate": 25.0,
|
||||
"locale_code": "hr-HR",
|
||||
"timezone": "Europe/Zagreb",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"country_code": "CY",
|
||||
"name": "Cyprus",
|
||||
"currency": "EUR",
|
||||
"default_vat_rate": 19.0,
|
||||
"locale_code": "el-CY",
|
||||
"timezone": "Asia/Nicosia",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"country_code": "CZ",
|
||||
"name": "Czech Republic",
|
||||
"currency": "CZK",
|
||||
"default_vat_rate": 21.0,
|
||||
"locale_code": "cs-CZ",
|
||||
"timezone": "Europe/Prague",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"country_code": "DK",
|
||||
"name": "Denmark",
|
||||
"currency": "DKK",
|
||||
"default_vat_rate": 25.0,
|
||||
"locale_code": "da-DK",
|
||||
"timezone": "Europe/Copenhagen",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"country_code": "EE",
|
||||
"name": "Estonia",
|
||||
"currency": "EUR",
|
||||
"default_vat_rate": 22.0,
|
||||
"locale_code": "et-EE",
|
||||
"timezone": "Europe/Tallinn",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"country_code": "FI",
|
||||
"name": "Finland",
|
||||
"currency": "EUR",
|
||||
"default_vat_rate": 25.5,
|
||||
"locale_code": "fi-FI",
|
||||
"timezone": "Europe/Helsinki",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"country_code": "FR",
|
||||
"name": "France",
|
||||
"currency": "EUR",
|
||||
"default_vat_rate": 20.0,
|
||||
"locale_code": "fr-FR",
|
||||
"timezone": "Europe/Paris",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"country_code": "DE",
|
||||
"name": "Germany",
|
||||
"currency": "EUR",
|
||||
"default_vat_rate": 19.0,
|
||||
"locale_code": "de-DE",
|
||||
"timezone": "Europe/Berlin",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"country_code": "GR",
|
||||
"name": "Greece",
|
||||
"currency": "EUR",
|
||||
"default_vat_rate": 24.0,
|
||||
"locale_code": "el-GR",
|
||||
"timezone": "Europe/Athens",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"country_code": "HU",
|
||||
"name": "Hungary",
|
||||
"currency": "HUF",
|
||||
"default_vat_rate": 27.0,
|
||||
"locale_code": "hu-HU",
|
||||
"timezone": "Europe/Budapest",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"country_code": "IE",
|
||||
"name": "Ireland",
|
||||
"currency": "EUR",
|
||||
"default_vat_rate": 23.0,
|
||||
"locale_code": "en-IE",
|
||||
"timezone": "Europe/Dublin",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"country_code": "IT",
|
||||
"name": "Italy",
|
||||
"currency": "EUR",
|
||||
"default_vat_rate": 22.0,
|
||||
"locale_code": "it-IT",
|
||||
"timezone": "Europe/Rome",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"country_code": "LV",
|
||||
"name": "Latvia",
|
||||
"currency": "EUR",
|
||||
"default_vat_rate": 21.0,
|
||||
"locale_code": "lv-LV",
|
||||
"timezone": "Europe/Riga",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"country_code": "LT",
|
||||
"name": "Lithuania",
|
||||
"currency": "EUR",
|
||||
"default_vat_rate": 21.0,
|
||||
"locale_code": "lt-LT",
|
||||
"timezone": "Europe/Vilnius",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"country_code": "LU",
|
||||
"name": "Luxembourg",
|
||||
"currency": "EUR",
|
||||
"default_vat_rate": 17.0,
|
||||
"locale_code": "lb-LU",
|
||||
"timezone": "Europe/Luxembourg",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"country_code": "MT",
|
||||
"name": "Malta",
|
||||
"currency": "EUR",
|
||||
"default_vat_rate": 18.0,
|
||||
"locale_code": "mt-MT",
|
||||
"timezone": "Europe/Malta",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"country_code": "NL",
|
||||
"name": "Netherlands",
|
||||
"currency": "EUR",
|
||||
"default_vat_rate": 21.0,
|
||||
"locale_code": "nl-NL",
|
||||
"timezone": "Europe/Amsterdam",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"country_code": "PL",
|
||||
"name": "Poland",
|
||||
"currency": "PLN",
|
||||
"default_vat_rate": 23.0,
|
||||
"locale_code": "pl-PL",
|
||||
"timezone": "Europe/Warsaw",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"country_code": "PT",
|
||||
"name": "Portugal",
|
||||
"currency": "EUR",
|
||||
"default_vat_rate": 23.0,
|
||||
"locale_code": "pt-PT",
|
||||
"timezone": "Europe/Lisbon",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"country_code": "RO",
|
||||
"name": "Romania",
|
||||
"currency": "RON",
|
||||
"default_vat_rate": 19.0,
|
||||
"locale_code": "ro-RO",
|
||||
"timezone": "Europe/Bucharest",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"country_code": "SK",
|
||||
"name": "Slovakia",
|
||||
"currency": "EUR",
|
||||
"default_vat_rate": 23.0,
|
||||
"locale_code": "sk-SK",
|
||||
"timezone": "Europe/Bratislava",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"country_code": "SI",
|
||||
"name": "Slovenia",
|
||||
"currency": "EUR",
|
||||
"default_vat_rate": 22.0,
|
||||
"locale_code": "sl-SI",
|
||||
"timezone": "Europe/Ljubljana",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"country_code": "ES",
|
||||
"name": "Spain",
|
||||
"currency": "EUR",
|
||||
"default_vat_rate": 21.0,
|
||||
"locale_code": "es-ES",
|
||||
"timezone": "Europe/Madrid",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"country_code": "SE",
|
||||
"name": "Sweden",
|
||||
"currency": "SEK",
|
||||
"default_vat_rate": 25.0,
|
||||
"locale_code": "sv-SE",
|
||||
"timezone": "Europe/Stockholm",
|
||||
"is_active": True,
|
||||
},
|
||||
# ── NON-EU EEA / ASSOCIATED ────────────────────────────────────────────
|
||||
{
|
||||
"country_code": "GB",
|
||||
"name": "United Kingdom",
|
||||
"currency": "GBP",
|
||||
"default_vat_rate": 20.0,
|
||||
"locale_code": "en-GB",
|
||||
"timezone": "Europe/London",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"country_code": "CH",
|
||||
"name": "Switzerland",
|
||||
"currency": "CHF",
|
||||
"default_vat_rate": 8.1,
|
||||
"locale_code": "de-CH",
|
||||
"timezone": "Europe/Zurich",
|
||||
"is_active": True,
|
||||
},
|
||||
# ── FALLBACK / DEFAULT ─────────────────────────────────────────────────
|
||||
{
|
||||
"country_code": "DEFAULT",
|
||||
"name": "Eurozone (Default)",
|
||||
"currency": "EUR",
|
||||
"default_vat_rate": 0.0,
|
||||
"locale_code": "en-EU",
|
||||
"timezone": "Europe/Brussels",
|
||||
"is_active": True,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
async def seed_regions():
|
||||
"""
|
||||
Seed the system.region_config table with all EU regions + GB + CH + DEFAULT.
|
||||
|
||||
Uses UPSERT logic: if a region with the same country_code already exists,
|
||||
its fields are UPDATED (name, currency, VAT rate, locale, timezone, is_active)
|
||||
rather than skipped. This ensures the script is idempotent and can be re-run
|
||||
to update VAT rates or other configuration changes.
|
||||
"""
|
||||
engine = create_async_engine(str(settings.SQLALCHEMY_DATABASE_URI), echo=False)
|
||||
async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
async with async_session() as session:
|
||||
async with session.begin():
|
||||
for region_data in DEFAULT_REGIONS:
|
||||
country_code = region_data["country_code"]
|
||||
|
||||
# Check if region already exists
|
||||
stmt = select(RegionConfig).where(
|
||||
RegionConfig.country_code == country_code
|
||||
)
|
||||
result = await session.execute(stmt)
|
||||
existing = result.scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
# UPSERT: update existing record with new values
|
||||
existing.name = region_data["name"]
|
||||
existing.currency = region_data["currency"]
|
||||
existing.default_vat_rate = region_data["default_vat_rate"]
|
||||
existing.locale_code = region_data["locale_code"]
|
||||
existing.timezone = region_data["timezone"]
|
||||
existing.is_active = region_data["is_active"]
|
||||
logger.info(
|
||||
f"🔄 Updated region: {country_code} - {region_data['name']} "
|
||||
f"(VAT: {region_data['default_vat_rate']}%, "
|
||||
f"Currency: {region_data['currency']})"
|
||||
)
|
||||
else:
|
||||
# INSERT: create new region
|
||||
region = RegionConfig(**region_data)
|
||||
session.add(region)
|
||||
logger.info(
|
||||
f"✅ Created region: {country_code} - {region_data['name']} "
|
||||
f"(VAT: {region_data['default_vat_rate']}%, "
|
||||
f"Currency: {region_data['currency']})"
|
||||
)
|
||||
|
||||
await session.commit()
|
||||
|
||||
# Count total regions
|
||||
count_stmt = select(RegionConfig)
|
||||
count_result = await session.execute(count_stmt)
|
||||
total = len(count_result.scalars().all())
|
||||
logger.info(f"🎉 Region seeding completed! Total regions in database: {total}")
|
||||
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(seed_regions())
|
||||
Reference in New Issue
Block a user