Replace all ~1,086 occurrences of Wizamart/wizamart/WIZAMART/WizaMart with Orion/orion/ORION across 184 files. This includes database identifiers, email addresses, domain references, R2 bucket names, DNS prefixes, encryption salt, Celery app name, config defaults, Docker configs, CI configs, documentation, seed data, and templates. Renames homepage-wizamart.html template to homepage-orion.html. Fixes duplicate file_pattern key in api.yaml architecture rule. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1275 lines
43 KiB
Python
1275 lines
43 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Demo Database Seeder for Orion Platform
|
|
|
|
Creates DEMO/TEST data for development and testing:
|
|
- Demo stores with realistic data
|
|
- Test customers and addresses
|
|
- Sample products
|
|
- Demo orders
|
|
- Store themes and custom domains
|
|
- Test import jobs
|
|
|
|
⚠️ WARNING: This script creates FAKE DATA for development only!
|
|
⚠️ NEVER run this in production!
|
|
|
|
Prerequisites:
|
|
- Database migrations must be applied (make migrate-up)
|
|
- Production initialization must be run (make init-prod)
|
|
|
|
Usage:
|
|
make seed-demo # Normal demo seeding
|
|
make seed-demo-minimal # Minimal seeding (1 store only)
|
|
make seed-demo-reset # Delete all data and reseed (DANGEROUS!)
|
|
make db-reset # Full reset (migrate down/up + init + seed reset)
|
|
|
|
Environment Variables:
|
|
SEED_MODE=normal|minimal|reset - Seeding mode (default: normal)
|
|
FORCE_RESET=true - Skip confirmation in reset mode (for non-interactive use)
|
|
|
|
This script is idempotent when run normally.
|
|
"""
|
|
|
|
import sys
|
|
from datetime import UTC, datetime
|
|
from decimal import Decimal
|
|
from pathlib import Path
|
|
|
|
# Add project root to path
|
|
project_root = Path(__file__).parent.parent
|
|
sys.path.insert(0, str(project_root))
|
|
|
|
# =============================================================================
|
|
# MODE DETECTION (from environment variable set by Makefile)
|
|
# =============================================================================
|
|
import contextlib
|
|
import os
|
|
|
|
from sqlalchemy import delete, select
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.core.config import settings
|
|
from app.core.database import SessionLocal
|
|
from app.core.environment import get_environment, is_production
|
|
from app.modules.catalog.models import Product
|
|
from app.modules.cms.models import ContentPage, StoreTheme
|
|
from app.modules.customers.models.customer import Customer, CustomerAddress
|
|
from app.modules.marketplace.models import (
|
|
MarketplaceImportJob,
|
|
MarketplaceProduct,
|
|
MarketplaceProductTranslation,
|
|
)
|
|
from app.modules.orders.models import Order, OrderItem
|
|
|
|
# =============================================================================
|
|
# MODEL IMPORTS
|
|
# =============================================================================
|
|
# ALL models must be imported before any ORM query so SQLAlchemy can resolve
|
|
# cross-module string relationships (e.g. Store→StoreEmailTemplate,
|
|
# Platform→SubscriptionTier, Product→Inventory).
|
|
# Core modules
|
|
from app.modules.tenancy.models import (
|
|
Merchant,
|
|
PlatformAlert,
|
|
Role,
|
|
Store,
|
|
StoreDomain,
|
|
StoreUser,
|
|
User,
|
|
)
|
|
from middleware.auth import AuthManager
|
|
|
|
# Optional modules — import to register models with SQLAlchemy
|
|
for _mod in [
|
|
"app.modules.inventory.models",
|
|
"app.modules.cart.models",
|
|
"app.modules.billing.models",
|
|
"app.modules.messaging.models",
|
|
"app.modules.loyalty.models",
|
|
]:
|
|
with contextlib.suppress(ImportError):
|
|
__import__(_mod)
|
|
|
|
SEED_MODE = os.getenv("SEED_MODE", "normal") # normal, minimal, reset
|
|
FORCE_RESET = os.getenv("FORCE_RESET", "false").lower() in ("true", "1", "yes")
|
|
|
|
# =============================================================================
|
|
# DEMO DATA CONFIGURATION
|
|
# =============================================================================
|
|
|
|
# Demo merchant configurations (NEW: Merchant-based architecture)
|
|
DEMO_COMPANIES = [
|
|
{
|
|
"name": "WizaCorp Ltd.",
|
|
"description": "Leading technology and electronics distributor",
|
|
"owner_email": "john.owner@wizacorp.com",
|
|
"owner_password": "password123",
|
|
"owner_first_name": "John",
|
|
"owner_last_name": "Smith",
|
|
"contact_email": "info@wizacorp.com",
|
|
"contact_phone": "+352 123 456 789",
|
|
"website": "https://www.wizacorp.com",
|
|
"business_address": "123 Tech Street, Luxembourg City, L-1234, Luxembourg",
|
|
"tax_number": "LU12345678",
|
|
},
|
|
{
|
|
"name": "Fashion Group S.A.",
|
|
"description": "International fashion and lifestyle retailer",
|
|
"owner_email": "jane.owner@fashiongroup.com",
|
|
"owner_password": "password123",
|
|
"owner_first_name": "Jane",
|
|
"owner_last_name": "Merchant",
|
|
"contact_email": "contact@fashiongroup.com",
|
|
"contact_phone": "+352 234 567 890",
|
|
"website": "https://www.fashiongroup.com",
|
|
"business_address": "456 Fashion Avenue, Luxembourg, L-5678, Luxembourg",
|
|
"tax_number": "LU23456789",
|
|
},
|
|
{
|
|
"name": "BookWorld Publishing",
|
|
"description": "Books, education, and media content provider",
|
|
"owner_email": "bob.owner@bookworld.com",
|
|
"owner_password": "password123",
|
|
"owner_first_name": "Bob",
|
|
"owner_last_name": "Seller",
|
|
"contact_email": "support@bookworld.com",
|
|
"contact_phone": "+352 345 678 901",
|
|
"website": "https://www.bookworld.com",
|
|
"business_address": "789 Library Lane, Esch-sur-Alzette, L-9012, Luxembourg",
|
|
"tax_number": "LU34567890",
|
|
},
|
|
]
|
|
|
|
# Demo store configurations (linked to merchants by index)
|
|
DEMO_STORES = [
|
|
{
|
|
"merchant_index": 0, # WizaCorp
|
|
"store_code": "ORION",
|
|
"name": "Orion",
|
|
"subdomain": "orion",
|
|
"description": "Premium electronics and gadgets marketplace",
|
|
"theme_preset": "modern",
|
|
"custom_domain": "orion.shop",
|
|
},
|
|
{
|
|
"merchant_index": 0, # WizaCorp
|
|
"store_code": "WIZAGADGETS",
|
|
"name": "WizaGadgets",
|
|
"subdomain": "wizagadgets",
|
|
"description": "Smart home devices and IoT accessories",
|
|
"theme_preset": "modern",
|
|
"custom_domain": None,
|
|
},
|
|
{
|
|
"merchant_index": 0, # WizaCorp
|
|
"store_code": "WIZAHOME",
|
|
"name": "WizaHome",
|
|
"subdomain": "wizahome",
|
|
"description": "Home appliances and kitchen electronics",
|
|
"theme_preset": "classic",
|
|
"custom_domain": None,
|
|
},
|
|
{
|
|
"merchant_index": 1, # Fashion Group
|
|
"store_code": "FASHIONHUB",
|
|
"name": "Fashion Hub",
|
|
"subdomain": "fashionhub",
|
|
"description": "Trendy clothing and accessories",
|
|
"theme_preset": "vibrant",
|
|
"custom_domain": "fashionhub.store",
|
|
},
|
|
{
|
|
"merchant_index": 1, # Fashion Group
|
|
"store_code": "FASHIONOUTLET",
|
|
"name": "Fashion Outlet",
|
|
"subdomain": "fashionoutlet",
|
|
"description": "Discounted designer fashion and seasonal clearance",
|
|
"theme_preset": "vibrant",
|
|
"custom_domain": None,
|
|
},
|
|
{
|
|
"merchant_index": 2, # BookWorld
|
|
"store_code": "BOOKSTORE",
|
|
"name": "The Book Store",
|
|
"subdomain": "bookstore",
|
|
"description": "Books, magazines, and educational materials",
|
|
"theme_preset": "classic",
|
|
"custom_domain": None,
|
|
},
|
|
{
|
|
"merchant_index": 2, # BookWorld
|
|
"store_code": "BOOKDIGITAL",
|
|
"name": "BookWorld Digital",
|
|
"subdomain": "bookdigital",
|
|
"description": "E-books, audiobooks, and digital learning resources",
|
|
"theme_preset": "modern",
|
|
"custom_domain": None,
|
|
},
|
|
]
|
|
|
|
# Demo team members (linked to merchants by index, assigned to stores by store_code)
|
|
DEMO_TEAM_MEMBERS = [
|
|
# WizaCorp team
|
|
{
|
|
"merchant_index": 0,
|
|
"email": "alice.manager@wizacorp.com",
|
|
"password": "password123",
|
|
"first_name": "Alice",
|
|
"last_name": "Manager",
|
|
"store_codes": ["ORION", "WIZAGADGETS"], # manages two stores
|
|
"user_type": "member",
|
|
},
|
|
{
|
|
"merchant_index": 0,
|
|
"email": "charlie.staff@wizacorp.com",
|
|
"password": "password123",
|
|
"first_name": "Charlie",
|
|
"last_name": "Staff",
|
|
"store_codes": ["WIZAHOME"],
|
|
"user_type": "member",
|
|
},
|
|
# Fashion Group team
|
|
{
|
|
"merchant_index": 1,
|
|
"email": "diana.stylist@fashiongroup.com",
|
|
"password": "password123",
|
|
"first_name": "Diana",
|
|
"last_name": "Stylist",
|
|
"store_codes": ["FASHIONHUB", "FASHIONOUTLET"],
|
|
"user_type": "member",
|
|
},
|
|
{
|
|
"merchant_index": 1,
|
|
"email": "eric.sales@fashiongroup.com",
|
|
"password": "password123",
|
|
"first_name": "Eric",
|
|
"last_name": "Sales",
|
|
"store_codes": ["FASHIONOUTLET"],
|
|
"user_type": "member",
|
|
},
|
|
# BookWorld team
|
|
{
|
|
"merchant_index": 2,
|
|
"email": "fiona.editor@bookworld.com",
|
|
"password": "password123",
|
|
"first_name": "Fiona",
|
|
"last_name": "Editor",
|
|
"store_codes": ["BOOKSTORE", "BOOKDIGITAL"],
|
|
"user_type": "member",
|
|
},
|
|
]
|
|
|
|
# Theme presets
|
|
THEME_PRESETS = {
|
|
"modern": {
|
|
"primary": "#3b82f6",
|
|
"secondary": "#06b6d4",
|
|
"accent": "#f59e0b",
|
|
"background": "#f9fafb",
|
|
"text": "#111827",
|
|
},
|
|
"classic": {
|
|
"primary": "#1e40af",
|
|
"secondary": "#7c3aed",
|
|
"accent": "#dc2626",
|
|
"background": "#ffffff",
|
|
"text": "#374151",
|
|
},
|
|
"vibrant": {
|
|
"primary": "#ec4899",
|
|
"secondary": "#f59e0b",
|
|
"accent": "#8b5cf6",
|
|
"background": "#fef3c7",
|
|
"text": "#78350f",
|
|
},
|
|
}
|
|
|
|
# Store content page overrides (demonstrates CMS store override feature)
|
|
# Each store can override platform default pages with custom content
|
|
STORE_CONTENT_PAGES = {
|
|
"ORION": [
|
|
{
|
|
"slug": "about",
|
|
"title": "About Orion",
|
|
"content": """
|
|
<div class="prose-content">
|
|
<h2>Welcome to Orion</h2>
|
|
<p>Your premier destination for cutting-edge electronics and innovative gadgets.</p>
|
|
|
|
<h3>Our Story</h3>
|
|
<p>Founded by tech enthusiasts, Orion has been bringing the latest technology to customers since 2020.
|
|
We carefully curate our selection to ensure you get only the best products at competitive prices.</p>
|
|
|
|
<h3>Why Choose Orion?</h3>
|
|
<ul>
|
|
<li><strong>Expert Selection:</strong> Our team tests and reviews every product</li>
|
|
<li><strong>Best Prices:</strong> We negotiate directly with manufacturers</li>
|
|
<li><strong>Fast Shipping:</strong> Same-day dispatch on orders before 2pm</li>
|
|
<li><strong>Tech Support:</strong> Free lifetime technical assistance</li>
|
|
</ul>
|
|
|
|
<h3>Visit Our Showroom</h3>
|
|
<p>123 Tech Street, Luxembourg City<br>Open Monday-Saturday, 9am-7pm</p>
|
|
</div>
|
|
""",
|
|
"meta_description": "Orion - Your trusted source for premium electronics and gadgets in Luxembourg",
|
|
"show_in_header": True,
|
|
"show_in_footer": True,
|
|
},
|
|
{
|
|
"slug": "contact",
|
|
"title": "Contact Orion",
|
|
"content": """
|
|
<div class="prose-content">
|
|
<h2>Get in Touch with Orion</h2>
|
|
|
|
<h3>Customer Support</h3>
|
|
<ul>
|
|
<li><strong>Email:</strong> support@orion.lu</li>
|
|
<li><strong>Phone:</strong> +352 123 456 789</li>
|
|
<li><strong>WhatsApp:</strong> +352 123 456 789</li>
|
|
<li><strong>Hours:</strong> Monday-Friday, 9am-6pm CET</li>
|
|
</ul>
|
|
|
|
<h3>Technical Support</h3>
|
|
<p>Need help with your gadgets? Our tech experts are here to help!</p>
|
|
<ul>
|
|
<li><strong>Email:</strong> tech@orion.lu</li>
|
|
<li><strong>Live Chat:</strong> Available on our website</li>
|
|
</ul>
|
|
|
|
<h3>Store Location</h3>
|
|
<p>123 Tech Street<br>Luxembourg City, L-1234<br>Luxembourg</p>
|
|
</div>
|
|
""",
|
|
"meta_description": "Contact Orion customer support for electronics and gadget inquiries",
|
|
"show_in_header": True,
|
|
"show_in_footer": True,
|
|
},
|
|
],
|
|
"FASHIONHUB": [
|
|
{
|
|
"slug": "about",
|
|
"title": "About Fashion Hub",
|
|
"content": """
|
|
<div class="prose-content">
|
|
<h2>Welcome to Fashion Hub</h2>
|
|
<p>Where style meets affordability. Discover the latest trends in fashion and accessories.</p>
|
|
|
|
<h3>Our Philosophy</h3>
|
|
<p>At Fashion Hub, we believe everyone deserves to look and feel their best.
|
|
We curate collections from emerging designers and established brands to bring you
|
|
fashion-forward pieces at accessible prices.</p>
|
|
|
|
<h3>What Makes Us Different</h3>
|
|
<ul>
|
|
<li><strong>Trend-Forward:</strong> New arrivals weekly from global fashion capitals</li>
|
|
<li><strong>Sustainable:</strong> 40% of our collection uses eco-friendly materials</li>
|
|
<li><strong>Inclusive:</strong> Sizes XS to 4XL available</li>
|
|
<li><strong>Personal Styling:</strong> Free virtual styling consultations</li>
|
|
</ul>
|
|
|
|
<h3>Join Our Community</h3>
|
|
<p>Follow us on Instagram @FashionHubLux for styling tips and exclusive offers!</p>
|
|
</div>
|
|
""",
|
|
"meta_description": "Fashion Hub - Trendy clothing and accessories for the style-conscious shopper",
|
|
"show_in_header": True,
|
|
"show_in_footer": True,
|
|
},
|
|
],
|
|
"BOOKSTORE": [
|
|
{
|
|
"slug": "about",
|
|
"title": "About The Book Store",
|
|
"content": """
|
|
<div class="prose-content">
|
|
<h2>Welcome to The Book Store</h2>
|
|
<p>Your literary haven in Luxembourg. From bestsellers to rare finds, we have something for every reader.</p>
|
|
|
|
<h3>Our Heritage</h3>
|
|
<p>Established by book lovers for book lovers, The Book Store has been serving
|
|
the Luxembourg community for over a decade. We pride ourselves on our carefully
|
|
curated selection and knowledgeable staff.</p>
|
|
|
|
<h3>What We Offer</h3>
|
|
<ul>
|
|
<li><strong>Vast Selection:</strong> Over 50,000 titles across all genres</li>
|
|
<li><strong>Special Orders:</strong> Can't find what you're looking for? We'll get it for you</li>
|
|
<li><strong>Book Club:</strong> Monthly meetings and 15% member discount</li>
|
|
<li><strong>Author Events:</strong> Regular readings and book signings</li>
|
|
<li><strong>Children's Corner:</strong> Dedicated space for young readers</li>
|
|
</ul>
|
|
|
|
<h3>Visit Us</h3>
|
|
<p>789 Library Lane, Esch-sur-Alzette<br>
|
|
Open daily 10am-8pm, Sundays 12pm-6pm</p>
|
|
</div>
|
|
""",
|
|
"meta_description": "The Book Store - Your independent bookshop in Luxembourg with a passion for literature",
|
|
"show_in_header": True,
|
|
"show_in_footer": True,
|
|
},
|
|
{
|
|
"slug": "faq",
|
|
"title": "Book Store FAQ",
|
|
"content": """
|
|
<div class="prose-content">
|
|
<h2>Frequently Asked Questions</h2>
|
|
|
|
<h3>Orders & Delivery</h3>
|
|
|
|
<h4>Do you ship internationally?</h4>
|
|
<p>Yes! We ship to all EU countries. Non-EU shipping available on request.</p>
|
|
|
|
<h4>How long does delivery take?</h4>
|
|
<p>Luxembourg: 1-2 business days. EU: 3-7 business days.</p>
|
|
|
|
<h4>Can I order books that aren't in stock?</h4>
|
|
<p>Absolutely! We can order any book in print. Special orders usually arrive within 1-2 weeks.</p>
|
|
|
|
<h3>Book Club</h3>
|
|
|
|
<h4>How do I join the book club?</h4>
|
|
<p>Sign up at our store or email bookclub@bookstore.lu. Annual membership is €25.</p>
|
|
|
|
<h4>What are the benefits?</h4>
|
|
<p>15% discount on all purchases, early access to author events, and monthly reading recommendations.</p>
|
|
|
|
<h3>Gift Cards</h3>
|
|
|
|
<h4>Do you sell gift cards?</h4>
|
|
<p>Yes! Available in €10, €25, €50, and €100 denominations, or custom amounts.</p>
|
|
</div>
|
|
""",
|
|
"meta_description": "Frequently asked questions about The Book Store - orders, delivery, and book club",
|
|
"show_in_header": False,
|
|
"show_in_footer": True,
|
|
},
|
|
],
|
|
}
|
|
|
|
|
|
# =============================================================================
|
|
# HELPER FUNCTIONS
|
|
# =============================================================================
|
|
|
|
|
|
def print_header(text: str):
|
|
"""Print formatted header."""
|
|
print("\n" + "=" * 70)
|
|
print(f" {text}")
|
|
print("=" * 70)
|
|
|
|
|
|
def print_step(step: int, text: str):
|
|
"""Print step indicator."""
|
|
print(f"\n[{step}] {text}")
|
|
|
|
|
|
def print_success(text: str):
|
|
"""Print success message."""
|
|
print(f" ✓ {text}")
|
|
|
|
|
|
def print_warning(text: str):
|
|
"""Print warning message."""
|
|
print(f" ⚠ {text}")
|
|
|
|
|
|
def print_error(text: str):
|
|
"""Print error message."""
|
|
print(f" ✗ {text}")
|
|
|
|
|
|
# =============================================================================
|
|
# SAFETY CHECKS
|
|
# =============================================================================
|
|
|
|
|
|
def check_environment():
|
|
"""Prevent running demo seed in production."""
|
|
|
|
if is_production():
|
|
print_error("Cannot run demo seeding in production!")
|
|
print(" This script creates FAKE DATA for development only.")
|
|
print(f" Current environment: {get_environment()}")
|
|
print("\n To seed in production:")
|
|
print(" 1. Set ENVIRONMENT=development in .env (or ENV=development)")
|
|
print(" 2. Run: make seed-demo")
|
|
sys.exit(1)
|
|
|
|
print_success(f"Environment check passed: {get_environment()}")
|
|
|
|
|
|
def check_admin_exists(db: Session) -> bool:
|
|
"""Check if admin user exists."""
|
|
|
|
admin = db.execute(select(User).where(User.role == "admin").limit(1)).scalar_one_or_none()
|
|
|
|
if not admin:
|
|
print_error("No admin user found!")
|
|
print(" Run production initialization first:")
|
|
print(" make init-prod")
|
|
return False
|
|
|
|
print_success(f"Admin user exists: {admin.email}")
|
|
return True
|
|
|
|
|
|
# =============================================================================
|
|
# DATA DELETION (for reset mode)
|
|
# =============================================================================
|
|
|
|
|
|
def reset_all_data(db: Session):
|
|
"""Delete ALL data from database (except admin user)."""
|
|
|
|
print_warning("RESETTING ALL DATA...")
|
|
print(" This will delete all stores, customers, orders, etc.")
|
|
print(" Admin user will be preserved.")
|
|
|
|
# Skip confirmation if FORCE_RESET is set (for non-interactive use)
|
|
if FORCE_RESET:
|
|
print_warning("FORCE_RESET enabled - skipping confirmation")
|
|
else:
|
|
# Get confirmation interactively
|
|
try:
|
|
response = input("\n Type 'DELETE ALL DATA' to confirm: ")
|
|
if response != "DELETE ALL DATA":
|
|
print(" Reset cancelled.")
|
|
sys.exit(0)
|
|
except EOFError:
|
|
print_error("No interactive terminal available.")
|
|
print(
|
|
" Use FORCE_RESET=true to skip confirmation in non-interactive mode."
|
|
)
|
|
sys.exit(1)
|
|
|
|
# Delete in correct order (respecting foreign keys)
|
|
tables_to_clear = [
|
|
OrderItem,
|
|
Order,
|
|
CustomerAddress,
|
|
Customer,
|
|
MarketplaceImportJob,
|
|
Product, # Product references MarketplaceProduct
|
|
MarketplaceProductTranslation, # Translation references MarketplaceProduct
|
|
MarketplaceProduct,
|
|
ContentPage, # Delete store content pages (keep platform defaults)
|
|
StoreDomain,
|
|
StoreTheme,
|
|
Role,
|
|
StoreUser,
|
|
Store,
|
|
Merchant, # Delete merchants (cascades to stores)
|
|
PlatformAlert,
|
|
]
|
|
|
|
for table in tables_to_clear:
|
|
if table == ContentPage:
|
|
# Only delete store content pages, keep platform defaults
|
|
db.execute(delete(ContentPage).where(ContentPage.store_id is not None))
|
|
else:
|
|
db.execute(delete(table))
|
|
|
|
# Delete non-admin users
|
|
db.execute(delete(User).where(User.role != "admin"))
|
|
|
|
db.commit()
|
|
print_success("All data deleted (admin preserved)")
|
|
|
|
|
|
# =============================================================================
|
|
# SEEDING FUNCTIONS
|
|
# =============================================================================
|
|
|
|
|
|
def create_demo_merchants(db: Session, auth_manager: AuthManager) -> list[Merchant]:
|
|
"""Create demo merchants with owner accounts."""
|
|
|
|
merchants = []
|
|
|
|
# Determine how many merchants to create based on mode
|
|
merchant_count = 1 if SEED_MODE == "minimal" else len(DEMO_COMPANIES)
|
|
merchants_to_create = DEMO_COMPANIES[:merchant_count]
|
|
|
|
for merchant_data in merchants_to_create:
|
|
# Check if merchant already exists
|
|
existing = db.execute(
|
|
select(Merchant).where(Merchant.name == merchant_data["name"])
|
|
).scalar_one_or_none()
|
|
|
|
if existing:
|
|
print_warning(f"Merchant already exists: {merchant_data['name']}")
|
|
merchants.append(existing)
|
|
continue
|
|
|
|
# Check if owner user already exists
|
|
owner_user = db.execute(
|
|
select(User).where(User.email == merchant_data["owner_email"])
|
|
).scalar_one_or_none()
|
|
|
|
if not owner_user:
|
|
# Create owner user
|
|
owner_user = User(
|
|
username=merchant_data["owner_email"].split("@")[0],
|
|
email=merchant_data["owner_email"],
|
|
hashed_password=auth_manager.hash_password(
|
|
merchant_data["owner_password"]
|
|
),
|
|
role="store",
|
|
first_name=merchant_data["owner_first_name"],
|
|
last_name=merchant_data["owner_last_name"],
|
|
is_active=True,
|
|
is_email_verified=True,
|
|
created_at=datetime.now(UTC),
|
|
updated_at=datetime.now(UTC),
|
|
)
|
|
db.add(owner_user)
|
|
db.flush()
|
|
print_success(
|
|
f"Created owner user: {owner_user.email} (password: {merchant_data['owner_password']})"
|
|
)
|
|
else:
|
|
print_warning(f"Using existing user as owner: {owner_user.email}")
|
|
|
|
# Create merchant
|
|
merchant = Merchant(
|
|
name=merchant_data["name"],
|
|
description=merchant_data["description"],
|
|
owner_user_id=owner_user.id,
|
|
contact_email=merchant_data["contact_email"],
|
|
contact_phone=merchant_data.get("contact_phone"),
|
|
website=merchant_data.get("website"),
|
|
business_address=merchant_data.get("business_address"),
|
|
tax_number=merchant_data.get("tax_number"),
|
|
is_active=True,
|
|
is_verified=True, # Auto-verified for demo
|
|
created_at=datetime.now(UTC),
|
|
updated_at=datetime.now(UTC),
|
|
)
|
|
db.add(merchant)
|
|
db.flush()
|
|
|
|
merchants.append(merchant)
|
|
print_success(f"Created merchant: {merchant.name} (Owner: {owner_user.email})")
|
|
|
|
db.flush()
|
|
return merchants
|
|
|
|
|
|
def create_demo_stores(
|
|
db: Session, merchants: list[Merchant], auth_manager: AuthManager
|
|
) -> list[Store]:
|
|
"""Create demo stores linked to merchants."""
|
|
|
|
stores = []
|
|
|
|
# Determine how many stores to create based on mode
|
|
store_count = 1 if SEED_MODE == "minimal" else len(DEMO_STORES)
|
|
stores_to_create = DEMO_STORES[:store_count]
|
|
|
|
for store_data in stores_to_create:
|
|
# Check if store already exists
|
|
existing = db.execute(
|
|
select(Store).where(Store.store_code == store_data["store_code"])
|
|
).scalar_one_or_none()
|
|
|
|
if existing:
|
|
print_warning(f"Store already exists: {store_data['name']}")
|
|
stores.append(existing)
|
|
continue
|
|
|
|
# Get merchant by index
|
|
merchant_index = store_data["merchant_index"]
|
|
if merchant_index >= len(merchants):
|
|
print_error(
|
|
f"Invalid merchant_index {merchant_index} for store {store_data['name']}"
|
|
)
|
|
continue
|
|
|
|
merchant = merchants[merchant_index]
|
|
|
|
# Create store linked to merchant (owner is inherited from merchant)
|
|
store = Store(
|
|
merchant_id=merchant.id, # Link to merchant
|
|
store_code=store_data["store_code"],
|
|
name=store_data["name"],
|
|
subdomain=store_data["subdomain"],
|
|
description=store_data["description"],
|
|
is_active=True,
|
|
is_verified=True,
|
|
created_at=datetime.now(UTC),
|
|
updated_at=datetime.now(UTC),
|
|
)
|
|
db.add(store)
|
|
db.flush()
|
|
|
|
# Link merchant owner to store as owner
|
|
store_user_link = StoreUser(
|
|
store_id=store.id,
|
|
user_id=merchant.owner_user_id,
|
|
user_type="owner",
|
|
is_active=True,
|
|
created_at=datetime.now(UTC),
|
|
)
|
|
db.add(store_user_link)
|
|
|
|
# Create store theme
|
|
theme_colors = THEME_PRESETS.get(
|
|
store_data["theme_preset"], THEME_PRESETS["modern"]
|
|
)
|
|
theme = StoreTheme(
|
|
store_id=store.id,
|
|
theme_name=store_data["theme_preset"],
|
|
colors={ # ✅ Use JSON format
|
|
"primary": theme_colors["primary"],
|
|
"secondary": theme_colors["secondary"],
|
|
"accent": theme_colors["accent"],
|
|
"background": theme_colors["background"],
|
|
"text": theme_colors["text"],
|
|
},
|
|
is_active=True,
|
|
created_at=datetime.now(UTC),
|
|
updated_at=datetime.now(UTC),
|
|
)
|
|
db.add(theme)
|
|
|
|
# Create custom domain if specified
|
|
if store_data.get("custom_domain"):
|
|
domain = StoreDomain(
|
|
store_id=store.id,
|
|
domain=store_data[
|
|
"custom_domain"
|
|
], # ✅ Field is 'domain', not 'domain_name'
|
|
is_verified=True, # Auto-verified for demo
|
|
is_primary=True,
|
|
verification_token=None,
|
|
created_at=datetime.now(UTC),
|
|
updated_at=datetime.now(UTC),
|
|
)
|
|
db.add(domain)
|
|
|
|
stores.append(store)
|
|
print_success(f"Created store: {store.name} ({store.store_code})")
|
|
|
|
db.flush()
|
|
return stores
|
|
|
|
|
|
def create_demo_team_members(
|
|
db: Session, stores: list[Store], auth_manager: AuthManager
|
|
) -> list[User]:
|
|
"""Create demo team member users and assign them to stores."""
|
|
|
|
if SEED_MODE == "minimal":
|
|
return []
|
|
|
|
team_users = []
|
|
# Build a store_code → Store lookup from the created stores
|
|
store_lookup = {s.store_code: s for s in stores}
|
|
|
|
for member_data in DEMO_TEAM_MEMBERS:
|
|
# Check if user already exists
|
|
user = db.execute(
|
|
select(User).where(User.email == member_data["email"])
|
|
).scalar_one_or_none()
|
|
|
|
if not user:
|
|
user = User(
|
|
username=member_data["email"].split("@")[0],
|
|
email=member_data["email"],
|
|
hashed_password=auth_manager.hash_password(member_data["password"]),
|
|
role="store",
|
|
first_name=member_data["first_name"],
|
|
last_name=member_data["last_name"],
|
|
is_active=True,
|
|
is_email_verified=True,
|
|
created_at=datetime.now(UTC),
|
|
updated_at=datetime.now(UTC),
|
|
)
|
|
db.add(user)
|
|
db.flush()
|
|
print_success(
|
|
f"Created team member: {user.email} (password: {member_data['password']})"
|
|
)
|
|
else:
|
|
print_warning(f"Team member already exists: {user.email}")
|
|
|
|
team_users.append(user)
|
|
|
|
# Assign user to stores
|
|
for store_code in member_data["store_codes"]:
|
|
store = store_lookup.get(store_code)
|
|
if not store:
|
|
print_warning(f"Store {store_code} not found, skipping assignment")
|
|
continue
|
|
|
|
# Check if StoreUser link already exists
|
|
existing_link = db.execute(
|
|
select(StoreUser).where(
|
|
StoreUser.store_id == store.id,
|
|
StoreUser.user_id == user.id,
|
|
)
|
|
).scalar_one_or_none()
|
|
|
|
if existing_link:
|
|
continue
|
|
|
|
store_user = StoreUser(
|
|
store_id=store.id,
|
|
user_id=user.id,
|
|
user_type=member_data["user_type"],
|
|
is_active=True,
|
|
created_at=datetime.now(UTC),
|
|
)
|
|
db.add(store_user)
|
|
print_success(f" Assigned {user.first_name} to {store.name} as {member_data['user_type']}")
|
|
|
|
db.flush()
|
|
return team_users
|
|
|
|
|
|
def create_demo_customers(
|
|
db: Session, store: Store, auth_manager: AuthManager, count: int
|
|
) -> list[Customer]:
|
|
"""Create demo customers for a store."""
|
|
|
|
customers = []
|
|
# Use a simple demo password for all customers
|
|
demo_password = "customer123"
|
|
|
|
for i in range(1, count + 1):
|
|
email = f"customer{i}@{store.subdomain}.example.com"
|
|
customer_number = f"CUST-{store.store_code}-{i:04d}"
|
|
|
|
# Check if customer already exists
|
|
existing_customer = (
|
|
db.query(Customer)
|
|
.filter(Customer.store_id == store.id, Customer.email == email)
|
|
.first()
|
|
)
|
|
|
|
if existing_customer:
|
|
customers.append(existing_customer)
|
|
continue # Skip creation, customer already exists
|
|
|
|
customer = Customer(
|
|
store_id=store.id,
|
|
email=email,
|
|
hashed_password=auth_manager.hash_password(demo_password),
|
|
first_name=f"Customer{i}",
|
|
last_name="Test",
|
|
phone=f"+352123456{i:03d}",
|
|
customer_number=customer_number,
|
|
is_active=True,
|
|
created_at=datetime.now(UTC),
|
|
updated_at=datetime.now(UTC),
|
|
)
|
|
db.add(customer)
|
|
customers.append(customer)
|
|
|
|
db.flush()
|
|
|
|
new_count = len([c for c in customers if c.id is None or db.is_modified(c)])
|
|
if new_count > 0:
|
|
print_success(f"Created {new_count} customers for {store.name}")
|
|
else:
|
|
print_warning(f"Customers already exist for {store.name}")
|
|
|
|
return customers
|
|
|
|
|
|
def create_demo_products(db: Session, store: Store, count: int) -> list[Product]:
|
|
"""Create demo products for a store."""
|
|
|
|
products = []
|
|
|
|
for i in range(1, count + 1):
|
|
marketplace_product_id = f"{store.store_code}-MP-{i:04d}"
|
|
product_id = f"{store.store_code}-PROD-{i:03d}"
|
|
|
|
# Check if this product already exists (by store_sku)
|
|
existing_product = (
|
|
db.query(Product)
|
|
.filter(Product.store_id == store.id, Product.store_sku == product_id)
|
|
.first()
|
|
)
|
|
|
|
if existing_product:
|
|
products.append(existing_product)
|
|
continue # Skip creation, product already exists
|
|
|
|
# Check if marketplace product already exists
|
|
existing_mp = (
|
|
db.query(MarketplaceProduct)
|
|
.filter(MarketplaceProduct.marketplace_product_id == marketplace_product_id)
|
|
.first()
|
|
)
|
|
|
|
if existing_mp:
|
|
marketplace_product = existing_mp
|
|
else:
|
|
# Create the MarketplaceProduct (base product data)
|
|
marketplace_product = MarketplaceProduct(
|
|
marketplace_product_id=marketplace_product_id,
|
|
source_url=f"https://{store.subdomain}.example.com/products/sample-{i}",
|
|
image_link=f"https://{store.subdomain}.example.com/images/product-{i}.jpg",
|
|
price=str(Decimal(f"{(i * 10) % 500 + 9.99}")), # Store as string
|
|
brand=store.name,
|
|
gtin=f"TEST{store.id:02d}{i:010d}",
|
|
availability="in stock",
|
|
condition="new",
|
|
google_product_category="Electronics > Computers > Laptops",
|
|
marketplace="Orion",
|
|
store_name=store.name,
|
|
currency="EUR",
|
|
created_at=datetime.now(UTC),
|
|
updated_at=datetime.now(UTC),
|
|
)
|
|
db.add(marketplace_product)
|
|
db.flush() # Flush to get the marketplace_product.id
|
|
|
|
# Check if English translation already exists
|
|
existing_translation = (
|
|
db.query(MarketplaceProductTranslation)
|
|
.filter(
|
|
MarketplaceProductTranslation.marketplace_product_id
|
|
== marketplace_product.id,
|
|
MarketplaceProductTranslation.language == "en",
|
|
)
|
|
.first()
|
|
)
|
|
|
|
if not existing_translation:
|
|
# Create English translation
|
|
translation = MarketplaceProductTranslation(
|
|
marketplace_product_id=marketplace_product.id,
|
|
language="en",
|
|
title=f"Sample Product {i} - {store.name}",
|
|
description=f"This is a demo product for testing purposes in {store.name}. High quality and affordable.",
|
|
)
|
|
db.add(translation)
|
|
|
|
# Create the Product (store-specific entry)
|
|
product = Product(
|
|
store_id=store.id,
|
|
marketplace_product_id=marketplace_product.id,
|
|
store_sku=product_id, # Use store_sku for store's internal product reference
|
|
price=float(Decimal(f"{(i * 10) % 500 + 9.99}")), # Store as float
|
|
is_active=True,
|
|
created_at=datetime.now(UTC),
|
|
updated_at=datetime.now(UTC),
|
|
)
|
|
db.add(product)
|
|
products.append(product)
|
|
|
|
db.flush()
|
|
|
|
new_count = len([p for p in products if p.id is None or db.is_modified(p)])
|
|
if new_count > 0:
|
|
print_success(f"Created {new_count} products for {store.name}")
|
|
else:
|
|
print_warning(f"Products already exist for {store.name}")
|
|
|
|
return products
|
|
|
|
|
|
def create_demo_store_content_pages(db: Session, stores: list[Store]) -> int:
|
|
"""Create store-specific content page overrides.
|
|
|
|
These demonstrate the CMS store override feature where stores can
|
|
customize platform default pages with their own branding and content.
|
|
"""
|
|
created_count = 0
|
|
|
|
# Get the OMS platform ID (stores are registered on OMS)
|
|
from app.modules.tenancy.models import Platform
|
|
|
|
oms_platform = db.execute(
|
|
select(Platform).where(Platform.code == "oms")
|
|
).scalar_one_or_none()
|
|
default_platform_id = oms_platform.id if oms_platform else 1
|
|
|
|
for store in stores:
|
|
store_pages = STORE_CONTENT_PAGES.get(store.store_code, [])
|
|
|
|
if not store_pages:
|
|
continue
|
|
|
|
for page_data in store_pages:
|
|
# Check if this store page already exists
|
|
existing = db.execute(
|
|
select(ContentPage).where(
|
|
ContentPage.store_id == store.id,
|
|
ContentPage.slug == page_data["slug"],
|
|
)
|
|
).scalar_one_or_none()
|
|
|
|
if existing:
|
|
continue # Skip, already exists
|
|
|
|
# Create store content page override
|
|
page = ContentPage(
|
|
platform_id=default_platform_id,
|
|
store_id=store.id,
|
|
slug=page_data["slug"],
|
|
title=page_data["title"],
|
|
content=page_data["content"].strip(),
|
|
content_format="html",
|
|
meta_description=page_data.get("meta_description"),
|
|
is_published=True,
|
|
published_at=datetime.now(UTC),
|
|
show_in_header=page_data.get("show_in_header", False),
|
|
show_in_footer=page_data.get("show_in_footer", True),
|
|
show_in_legal=page_data.get("show_in_legal", False),
|
|
display_order=page_data.get("display_order", 0),
|
|
created_at=datetime.now(UTC),
|
|
updated_at=datetime.now(UTC),
|
|
)
|
|
db.add(page)
|
|
created_count += 1
|
|
|
|
db.flush()
|
|
|
|
if created_count > 0:
|
|
print_success(f"Created {created_count} store content page overrides")
|
|
else:
|
|
print_warning("Store content pages already exist")
|
|
|
|
return created_count
|
|
|
|
|
|
# =============================================================================
|
|
# MAIN SEEDING
|
|
# =============================================================================
|
|
|
|
|
|
def seed_demo_data(db: Session, auth_manager: AuthManager):
|
|
"""Seed demo data for development."""
|
|
|
|
print_header("DEMO DATA SEEDING")
|
|
print(f" Mode: {SEED_MODE.upper()}")
|
|
|
|
# Step 1: Check environment
|
|
print_step(1, "Checking environment...")
|
|
check_environment()
|
|
|
|
# Step 2: Check admin exists
|
|
print_step(2, "Verifying admin user...")
|
|
if not check_admin_exists(db):
|
|
sys.exit(1)
|
|
|
|
# Step 3: Reset data if in reset mode
|
|
if SEED_MODE == "reset":
|
|
print_step(3, "Resetting data...")
|
|
reset_all_data(db)
|
|
|
|
# Step 4: Create merchants
|
|
print_step(4, "Creating demo merchants...")
|
|
merchants = create_demo_merchants(db, auth_manager)
|
|
|
|
# Step 5: Create stores
|
|
print_step(5, "Creating demo stores...")
|
|
stores = create_demo_stores(db, merchants, auth_manager)
|
|
|
|
# Step 6: Create team members
|
|
print_step(6, "Creating demo team members...")
|
|
create_demo_team_members(db, stores, auth_manager)
|
|
|
|
# Step 7: Create customers
|
|
print_step(7, "Creating demo customers...")
|
|
for store in stores:
|
|
create_demo_customers(
|
|
db, store, auth_manager, count=settings.seed_customers_per_store
|
|
)
|
|
|
|
# Step 8: Create products
|
|
print_step(8, "Creating demo products...")
|
|
for store in stores:
|
|
create_demo_products(db, store, count=settings.seed_products_per_store)
|
|
|
|
# Step 9: Create store content pages
|
|
print_step(9, "Creating store content page overrides...")
|
|
create_demo_store_content_pages(db, stores)
|
|
|
|
# Commit all changes
|
|
db.commit()
|
|
print_success("All demo data committed")
|
|
|
|
|
|
def print_summary(db: Session):
|
|
"""Print seeding summary."""
|
|
|
|
print_header("SEEDING SUMMARY")
|
|
|
|
# Count records
|
|
merchant_count = db.query(Merchant).count()
|
|
store_count = db.query(Store).count()
|
|
user_count = db.query(User).count()
|
|
team_member_count = db.query(StoreUser).filter(StoreUser.user_type == "member").count()
|
|
customer_count = db.query(Customer).count()
|
|
product_count = db.query(Product).count()
|
|
platform_pages = db.query(ContentPage).filter(ContentPage.store_id is None).count()
|
|
store_pages = db.query(ContentPage).filter(ContentPage.store_id is not None).count()
|
|
|
|
print("\n📊 Database Status:")
|
|
print(f" Merchants: {merchant_count}")
|
|
print(f" Stores: {store_count}")
|
|
print(f" Users: {user_count}")
|
|
print(f" Team memberships: {team_member_count}")
|
|
print(f" Customers: {customer_count}")
|
|
print(f" Products: {product_count}")
|
|
print(f" Content Pages: {platform_pages} platform + {store_pages} store overrides")
|
|
|
|
# Show merchant details
|
|
merchants = db.query(Merchant).all()
|
|
print("\n🏢 Demo Merchants:")
|
|
for merchant in merchants:
|
|
print(f"\n {merchant.name}")
|
|
print(f" Owner: {merchant.owner.email if merchant.owner else 'N/A'}")
|
|
print(f" Stores: {len(merchant.stores) if merchant.stores else 0}")
|
|
print(f" Status: {'✓ Active' if merchant.is_active else '✗ Inactive'}")
|
|
if merchant.is_verified:
|
|
print(" Verified: ✓")
|
|
|
|
# Show store details with team members
|
|
stores = db.query(Store).all()
|
|
print("\n🏪 Demo Stores:")
|
|
for store in stores:
|
|
print(f"\n {store.name} ({store.store_code})")
|
|
print(f" Subdomain: {store.subdomain}.{settings.platform_domain}")
|
|
|
|
# Query custom domains separately
|
|
custom_domain = (
|
|
db.query(StoreDomain)
|
|
.filter(StoreDomain.store_id == store.id, StoreDomain.is_active == True)
|
|
.first()
|
|
)
|
|
|
|
if custom_domain:
|
|
# Try different possible field names (model field might vary)
|
|
domain_value = (
|
|
getattr(custom_domain, "domain", None)
|
|
or getattr(custom_domain, "domain_name", None)
|
|
or getattr(custom_domain, "name", None)
|
|
)
|
|
if domain_value:
|
|
print(f" Custom: {domain_value}")
|
|
|
|
print(f" Status: {'✓ Active' if store.is_active else '✗ Inactive'}")
|
|
|
|
# Show team members for this store
|
|
store_users = (
|
|
db.query(StoreUser)
|
|
.filter(StoreUser.store_id == store.id)
|
|
.all()
|
|
)
|
|
if store_users:
|
|
for su in store_users:
|
|
user = db.query(User).filter(User.id == su.user_id).first()
|
|
if user:
|
|
print(f" Team: {user.email} ({su.user_type})")
|
|
|
|
print("\n🔐 Demo Merchant Owner Credentials:")
|
|
print("─" * 70)
|
|
for i, merchant_data in enumerate(DEMO_COMPANIES[:merchant_count], 1):
|
|
merchant = merchants[i - 1] if i <= len(merchants) else None
|
|
print(f" Merchant {i}: {merchant_data['name']}")
|
|
print(f" Email: {merchant_data['owner_email']}")
|
|
print(f" Password: {merchant_data['owner_password']}")
|
|
if merchant and merchant.stores:
|
|
for store in merchant.stores:
|
|
print(
|
|
f" Store: http://localhost:8000/store/{store.store_code}/login"
|
|
)
|
|
print(
|
|
f" or http://{store.subdomain}.localhost:8000/store/login"
|
|
)
|
|
print()
|
|
|
|
print("\n👥 Demo Team Member Credentials:")
|
|
print("─" * 70)
|
|
for member_data in DEMO_TEAM_MEMBERS:
|
|
merchant_name = DEMO_COMPANIES[member_data["merchant_index"]]["name"]
|
|
store_codes = ", ".join(member_data["store_codes"])
|
|
print(f" {member_data['first_name']} {member_data['last_name']} ({merchant_name})")
|
|
print(f" Email: {member_data['email']}")
|
|
print(f" Password: {member_data['password']}")
|
|
print(f" Stores: {store_codes}")
|
|
print()
|
|
|
|
print("\n🛒 Demo Customer Credentials:")
|
|
print("─" * 70)
|
|
print(" All customers:")
|
|
print(" Email: customer1@{subdomain}.example.com")
|
|
print(" Password: customer123")
|
|
print(" (Replace {subdomain} with store subdomain, e.g., orion)")
|
|
print()
|
|
|
|
print("\n🏪 Shop Access (Development):")
|
|
print("─" * 70)
|
|
for store in stores:
|
|
print(f" {store.name}:")
|
|
print(
|
|
f" Path-based: http://localhost:8000/stores/{store.store_code}/shop/"
|
|
)
|
|
print(f" Subdomain: http://{store.subdomain}.localhost:8000/")
|
|
print()
|
|
|
|
print("⚠️ ALL DEMO CREDENTIALS ARE INSECURE - For development only!")
|
|
|
|
print("\n🚀 NEXT STEPS:")
|
|
print(" 1. Start development: make dev")
|
|
print(" 2. Login as store:")
|
|
print(" • Path-based: http://localhost:8000/store/ORION/login")
|
|
print(" • Subdomain: http://orion.localhost:8000/store/login")
|
|
print(" 3. Visit store shop: http://localhost:8000/stores/ORION/shop/")
|
|
print(" 4. Admin panel: http://localhost:8000/admin/login")
|
|
print(f" Username: {settings.admin_username}")
|
|
print(f" Password: {settings.admin_password}")
|
|
|
|
|
|
# =============================================================================
|
|
# MAIN ENTRY POINT
|
|
# =============================================================================
|
|
|
|
|
|
def main():
|
|
"""Main entry point."""
|
|
|
|
print("\n" + "╔" + "═" * 68 + "╗")
|
|
print("║" + " " * 20 + "DEMO DATA SEEDING" + " " * 31 + "║")
|
|
print("╚" + "═" * 68 + "╝")
|
|
|
|
db = SessionLocal()
|
|
auth_manager = AuthManager()
|
|
|
|
try:
|
|
seed_demo_data(db, auth_manager)
|
|
print_summary(db)
|
|
|
|
print_header("✅ DEMO SEEDING COMPLETED")
|
|
|
|
except KeyboardInterrupt:
|
|
db.rollback()
|
|
print("\n\n⚠️ Seeding interrupted")
|
|
sys.exit(1)
|
|
|
|
except Exception as e:
|
|
db.rollback()
|
|
print_header("❌ SEEDING FAILED")
|
|
print(f"\nError: {e}\n")
|
|
import traceback
|
|
|
|
traceback.print_exc()
|
|
sys.exit(1)
|
|
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|