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

472 lines
22 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
# 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()