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