import pymysql from sqlalchemy import create_engine, select, text from sqlalchemy.orm import Session from urllib.parse import urlparse import ulid from app.core.config.Config import settings from app.core.database.db_session import ( Base, engine_core, engine_crm, engine_commerce, SessionLocal ) import app.models.db_base # Ensure new CMS models are imported so their tables are included in metadata import app.models.StorefrontCmsModel # noqa: F401 from app.models.RoleModel import Role from app.models.DepartmentModel import Department from app.models.DesignationModel import Designation from app.models.UserModel import User from app.models.SettingModel import Setting from app.models.StorefrontCmsModel import StorefrontSettings, StorefrontFooterInfo from app.utils.Hash_util import hash_password def verify_or_create_database(db_url: str): try: parsed = urlparse(db_url) db_name = parsed.path.lstrip("/") host = parsed.hostname or "127.0.0.1" port = parsed.port or 3306 user = parsed.username or "root" password = parsed.password or "" print(f"Verifying/creating database '{db_name}' on MySQL host {host}:{port}...") connection = pymysql.connect( host=host, port=port, user=user, password=password ) try: with connection.cursor() as cursor: cursor.execute(f"CREATE DATABASE IF NOT EXISTS `{db_name}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;") connection.commit() finally: connection.close() except Exception as err: print(f"Database verification notice (continuing safely): {err}") def initialize_database(): # 1. Verify/create all three database schemas verify_or_create_database(settings.CORE_DATABASE_URL or settings.DATABASE_URL) verify_or_create_database(settings.CRM_DATABASE_URL or settings.DATABASE_URL) verify_or_create_database(settings.COMMERCE_DATABASE_URL or settings.DATABASE_URL) # Safely recreate settings table if schema was updated with engine_core.connect() as conn: try: conn.execute(text("SELECT created_at FROM settings LIMIT 1")) except Exception: try: conn.execute(text("DROP TABLE IF EXISTS settings")) conn.commit() except Exception: pass # 2. Extract tables belonging to each DB in dependency-sorted order crm_tables = [ "service_types", "repair_services", "repair_variants", "repair_variant_images", "parts", "part_device_compatibility", "repair_variant_parts", "stock_movements", "purchase_orders", "purchase_order_items", "contacts", "contact_addresses" ] core_tables = [ "departments", "designations", "roles", "permissions", "users", "user_sessions", "audit_logs", "countries", "states", "cities", "settings", "file_uploads", "role_permissions" ] core_metadata_tables = [t for t in Base.metadata.sorted_tables if t.name in core_tables] crm_metadata_tables = [t for t in Base.metadata.sorted_tables if t.name in crm_tables] commerce_metadata_tables = [ t for t in Base.metadata.sorted_tables if t.name not in core_tables and t.name not in crm_tables ] print("Creating tables in Core Database...") Base.metadata.create_all(bind=engine_core, tables=core_metadata_tables) print("Creating tables in CRM Database...") Base.metadata.create_all(bind=engine_crm, tables=crm_metadata_tables) print("Creating tables in Commerce Database...") Base.metadata.create_all(bind=engine_commerce, tables=commerce_metadata_tables) # 2.5 Run auto-migrations for new columns on existing tables in Commerce DB from sqlalchemy import text with engine_commerce.connect() as conn: # device_series try: conn.execute(text("SELECT device_type FROM device_series LIMIT 1")) except Exception: print("Auto-Migration: Adding 'device_type' column to 'device_series' table...") try: conn.execute(text("ALTER TABLE device_series ADD COLUMN device_type VARCHAR(50) NULL")) conn.commit() except Exception as e: print(f"Failed to alter device_series: {e}") # device_models try: conn.execute(text("SELECT device_type FROM device_models LIMIT 1")) except Exception: print("Auto-Migration: Adding 'device_type' column to 'device_models' table...") try: conn.execute(text("ALTER TABLE device_models ADD COLUMN device_type VARCHAR(50) NULL")) conn.commit() except Exception as e: print(f"Failed to alter device_models: {e}") # device_models series_id nullable migration try: print("Auto-Migration: Modifying 'device_models.series_id' to be NULLable...") conn.execute(text("ALTER TABLE device_models MODIFY COLUMN series_id VARCHAR(26) NULL")) conn.commit() except Exception as e: print(f"Failed to modify device_models.series_id: {e}") # products try: conn.execute(text("SELECT device_type FROM products LIMIT 1")) except Exception: print("Auto-Migration: Adding 'device_type' column to 'products' table...") try: conn.execute(text("ALTER TABLE products ADD COLUMN device_type VARCHAR(50) NULL")) conn.commit() except Exception as e: print(f"Failed to alter products: {e}") # products category_id NULLable try: conn.execute(text("SET FOREIGN_KEY_CHECKS=0;")) conn.execute(text("ALTER TABLE products MODIFY COLUMN category_id VARCHAR(26) NULL")) conn.execute(text("ALTER TABLE device_series MODIFY COLUMN brand_id VARCHAR(26) NULL")) conn.execute(text("ALTER TABLE device_models MODIFY COLUMN brand_id VARCHAR(26) NULL")) conn.execute(text("SET FOREIGN_KEY_CHECKS=1;")) conn.commit() except Exception: pass # service_jobs logistics columns try: conn.execute(text("SELECT courier_name FROM service_jobs LIMIT 1")) except Exception: print("Auto-Migration: Adding logistics columns to 'service_jobs' table...") try: conn.execute(text("ALTER TABLE service_jobs ADD COLUMN courier_name VARCHAR(100) NULL")) conn.execute(text("ALTER TABLE service_jobs ADD COLUMN awb_number VARCHAR(100) NULL")) conn.execute(text("ALTER TABLE service_jobs ADD COLUMN pickup_status VARCHAR(50) NULL")) conn.commit() except Exception as e: print(f"Failed to alter service_jobs: {e}") # attribute_types (preset_values) try: conn.execute(text("SELECT preset_values FROM attribute_types LIMIT 1")) except Exception: print("Auto-Migration: Adding 'preset_values' column to 'attribute_types' table...") try: conn.execute(text("ALTER TABLE attribute_types ADD COLUMN preset_values JSON NULL")) conn.commit() except Exception as e: print(f"Failed to alter attribute_types: {e}") # customer_devices (storage_capacity) try: conn.execute(text("SELECT storage_capacity FROM customer_devices LIMIT 1")) except Exception: print("Auto-Migration: Adding 'storage_capacity' column to 'customer_devices' table...") try: conn.execute(text("ALTER TABLE customer_devices ADD COLUMN storage_capacity VARCHAR(100) NULL")) conn.commit() except Exception as e: print(f"Failed to alter customer_devices: {e}") # service_jobs (custom_service_name) try: conn.execute(text("SELECT custom_service_name FROM service_jobs LIMIT 1")) except Exception: print("Auto-Migration: Adding 'custom_service_name' column to 'service_jobs' table...") try: conn.execute(text("ALTER TABLE service_jobs ADD COLUMN custom_service_name VARCHAR(255) NULL")) conn.commit() except Exception as e: print(f"Failed to alter service_jobs: {e}") # migration_jobs table schema parity auto-migration try: conn.execute(text("SELECT current_phase FROM migration_jobs LIMIT 1")) except Exception: print("Auto-Migration: Adding missing worker & progress columns to 'migration_jobs' table...") migration_jobs_alters = [ "ALTER TABLE migration_jobs ADD COLUMN current_phase VARCHAR(50) NOT NULL DEFAULT 'UPLOAD'", "ALTER TABLE migration_jobs ADD COLUMN worker_id VARCHAR(64) NULL", "ALTER TABLE migration_jobs ADD COLUMN locked_at DATETIME NULL", "ALTER TABLE migration_jobs ADD COLUMN heartbeat_at DATETIME NULL", "ALTER TABLE migration_jobs ADD COLUMN lease_version BIGINT NOT NULL DEFAULT 1", "ALTER TABLE migration_jobs ADD COLUMN cancel_requested_at DATETIME NULL", "ALTER TABLE migration_jobs ADD COLUMN current_batch INT NOT NULL DEFAULT 0", "ALTER TABLE migration_jobs ADD COLUMN total_batches INT NOT NULL DEFAULT 0", "ALTER TABLE migration_jobs ADD COLUMN last_successful_batch INT NOT NULL DEFAULT 0", "ALTER TABLE migration_jobs ADD COLUMN warning_records INT NOT NULL DEFAULT 0", "ALTER TABLE migration_jobs ADD COLUMN retry_count INT NOT NULL DEFAULT 0", "ALTER TABLE migration_jobs ADD COLUMN expected_products INT NOT NULL DEFAULT 0", "ALTER TABLE migration_jobs ADD COLUMN expected_variants INT NOT NULL DEFAULT 0", "ALTER TABLE migration_jobs ADD COLUMN expected_media_items INT NOT NULL DEFAULT 0", "ALTER TABLE migration_jobs ADD COLUMN expected_media_links INT NOT NULL DEFAULT 0", "ALTER TABLE migration_jobs ADD COLUMN started_at DATETIME NULL", "ALTER TABLE migration_jobs ADD COLUMN completed_at DATETIME NULL", "ALTER TABLE migration_jobs ADD COLUMN failed_at DATETIME NULL", "ALTER TABLE migration_jobs ADD COLUMN finished_at DATETIME NULL", "ALTER TABLE migration_jobs ADD COLUMN error_message TEXT NULL", ] for alter_sql in migration_jobs_alters: try: conn.execute(text(alter_sql)) conn.commit() except Exception as e: print(f"Migration column alter note: {e}") # media_library table schema parity auto-migration try: conn.execute(text("SELECT storage_path FROM media_library LIMIT 1")) except Exception: print("Auto-Migration: Adding missing columns to 'media_library' table...") media_library_alters = [ "ALTER TABLE media_library ADD COLUMN storage_path VARCHAR(512) NULL", "ALTER TABLE media_library ADD COLUMN exif_metadata JSON NULL", "ALTER TABLE media_library ADD COLUMN cdn_url VARCHAR(512) NULL", "ALTER TABLE media_library ADD COLUMN thumbnail_url VARCHAR(512) NULL", ] for alter_sql in media_library_alters: try: conn.execute(text(alter_sql)) conn.commit() except Exception as e: print(f"Media library column alter note: {e}") # categories — CMS columns (show_in_sidebar, mega_group, badge) try: conn.execute(text("SELECT show_in_sidebar FROM categories LIMIT 1")) except Exception: print("Auto-Migration: Adding CMS columns to 'categories' table...") for alter_sql in [ "ALTER TABLE categories ADD COLUMN show_in_sidebar TINYINT(1) NOT NULL DEFAULT 1", "ALTER TABLE categories ADD COLUMN mega_group VARCHAR(64) NULL", "ALTER TABLE categories ADD COLUMN badge VARCHAR(32) NULL", ]: try: conn.execute(text(alter_sql)) conn.commit() except Exception as e: print(f"Category CMS column alter note: {e}") print("All tables compiled and created successfully in their respective databases.") # 3. Seed only the minimum bootstrap data needed to start the platform db = SessionLocal() try: seed_bootstrap(db) finally: db.close() def seed_bootstrap(db: Session): """ Seeds only the absolute minimum data required to boot the platform: - System settings (media / store controls) - Super Admin role, Administration department, General Administrator designation — required as FK dependencies for the admin user - One Super Admin user account All business data (brands, categories, products, extra roles / departments / designations, etc.) must be entered via the Admin UI after first login. """ print("Seeding bootstrap system settings...") # ── System Settings ──────────────────────────────────────────────────── settings_data = [ { "setting_key": "GLOBAL_DISABLE", "setting_value": {"enabled": False}, "description": "Global Kill Switch Controls", "is_public": False, }, { "setting_key": "STORE_CLOSED", "setting_value": {"closed": False, "reason": ""}, "description": "Storefront Operations Controls", "is_public": True, }, { "setting_key": "media_store_original", "setting_value": {"value": True}, "description": "Store raw original image file alongside WebP", "is_public": False, }, { "setting_key": "media_max_size_mb", "setting_value": {"value": 20}, "description": "Maximum upload file size in MB", "is_public": True, }, { "setting_key": "media_webp_quality", "setting_value": {"value": 88}, "description": "WebP compression quality (80-100)", "is_public": False, }, { "setting_key": "media_cleanup_enabled", "setting_value": {"value": True}, "description": "Async media garbage collection kill switch", "is_public": False, }, { "setting_key": "media_cleanup_grace_hours", "setting_value": {"value": 24}, "description": "Safety grace period before physical file deletion in hours", "is_public": False, }, ] for s in settings_data: existing = db.execute( select(Setting).where(Setting.setting_key == s["setting_key"]) ).scalar_one_or_none() if not existing: db.add(Setting( setting_id=str(ulid.ULID()), setting_key=s["setting_key"], group="system", type="json", setting_value=s["setting_value"], description=s["description"], is_public=s["is_public"], )) print(f" + Setting: {s['setting_key']}") db.commit() # ── Super Admin Role (FK dependency for admin user) ──────────────────── super_admin_role = db.execute( select(Role).where(Role.role_name == "Super Admin") ).scalar_one_or_none() if not super_admin_role: super_admin_role = Role( role_id=str(ulid.ULID()), role_name="Super Admin", role_prefix="ADM", description="System Super Administrator", is_system=True, is_active=True, ) db.add(super_admin_role) db.commit() print(" + Role: Super Admin") # ── Administration Department (FK dependency for admin user) ─────────── admin_dept = db.execute( select(Department).where(Department.name == "Administration") ).scalar_one_or_none() if not admin_dept: admin_dept = Department( department_id=str(ulid.ULID()), name="Administration", description="Global Admin Department", is_active=True, ) db.add(admin_dept) db.commit() print(" + Department: Administration") # ── General Administrator Designation (FK dependency for admin user) ─── admin_desig = db.execute( select(Designation).where(Designation.name == "General Administrator") ).scalar_one_or_none() if not admin_desig: admin_desig = Designation( designation_id=str(ulid.ULID()), name="General Administrator", description="Platform Operations Manager", is_active=True, ) db.add(admin_desig) db.commit() print(" + Designation: General Administrator") # ── Super Admin User ─────────────────────────────────────────────────── super_admin_email = "admin@ifixkart.com" existing_admin = db.execute( select(User).where(User.email == super_admin_email) ).scalar_one_or_none() if not existing_admin: db.add(User( user_id=str(ulid.ULID()), employee_code="ADM0001", first_name="iFixKart", last_name="Administrator", display_name="iFixKart Admin", email=super_admin_email, phone="1000000000", password_hash=hash_password("Admin$2026Setup"), department_id=admin_dept.department_id, designation_id=admin_desig.designation_id, role_id=super_admin_role.role_id, is_active=True, email_verified=True, phone_verified=True, )) db.commit() print(f" + Super Admin: {super_admin_email} (password: Admin$2026Setup)") # ── Storefront Footer Info default ──────────────────────────────────── existing_footer = db.get(StorefrontFooterInfo, "default") if not existing_footer: db.add(StorefrontFooterInfo( id="default", phone="+91 99999 99999", email="support@ifixkart.com", address="iFixKart Service Center, MG Road, Bengaluru, Karnataka - 560001", copyright="© 2026 iFixKart. All rights reserved.", social_links=[ {"platform": "Facebook", "url": "https://facebook.com/ifixkart", "icon": "facebook"}, {"platform": "Instagram", "url": "https://instagram.com/ifixkart", "icon": "instagram"}, {"platform": "Twitter", "url": "https://twitter.com/ifixkart", "icon": "twitter"}, ], columns=[ {"title": "Get to Know Us", "links": [{"label": "About Us", "href": "/about"}, {"label": "Term & Policy", "href": "/terms"}, {"label": "Careers", "href": "/careers"}, {"label": "News & Blog", "href": "/blog"}, {"label": "Contact Us", "href": "/contact"}]}, {"title": "Information", "links": [{"label": "Help Center", "href": "/help"}, {"label": "Feedback", "href": "/feedback"},{"label": "FAQs", "href": "/faqs"}, {"label": "Payments", "href": "/payments"}]}, {"title": "Orders & Returns","links": [{"label": "Track Order","href": "/account"},{"label": "Delivery", "href": "/delivery"},{"label": "Services", "href": "/services"},{"label": "Returns", "href": "/returns"}]}, {"title": "Our Store", "links": [{"label": "Best Seller", "href": "/products?sort=best-sellers"},{"label": "New Products","href": "/products?sort=newest"},{"label": "On Sale","href": "/products?on_sale=true"},{"label": "Featured","href": "/products?featured=true"}]}, ], payment_methods=[ {"name": "Visa", "icon_url": "/images/payments/visa.svg"}, {"name": "Mastercard", "icon_url": "/images/payments/mastercard.svg"}, {"name": "UPI", "icon_url": "/images/payments/upi.svg"}, {"name": "Razorpay", "icon_url": "/images/payments/razorpay.svg"}, ], )) db.commit() print(" + Storefront Footer Info: default record seeded") # ── Storefront Settings defaults ────────────────────────────────────── default_cms_settings = [ ("store_name", "iFixKart"), ("logo_url", "/images/logo/ifixkart-logo.webp"), ("primary_wordmark_url", "/images/logo/ifixkart-wordmark-primary.webp"), ("secondary_wordmark_url", "/images/logo/ifixkart-wordmark-secondary.webp"), ("favicon_url", "/favicon.ico"), ("support_phone", "+91 99999 99999"), ("currency_code", "INR"), ("advance_percent", 20.0), ("theme_color", "#6D28D9"), ] for key, value in default_cms_settings: existing = db.get(StorefrontSettings, key) if not existing: db.add(StorefrontSettings(key=key, value=value, updated_by="system")) print(f" + StorefrontSettings: {key}") db.commit() print("Bootstrap complete. All business data must be added via the Admin UI.") if __name__ == "__main__": initialize_database()