ifixkart-backend/Backend/app/core/database/init_db.py

391 lines
17 KiB
Python

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
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.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
# 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}")
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)")
print("Bootstrap complete. All business data must be added via the Admin UI.")
if __name__ == "__main__":
initialize_database()