refactor(scripts): reorganize scripts/ into seed/ and validate/ subfolders

Move 9 init/seed scripts into scripts/seed/ and 7 validation scripts
(+ validators/ subfolder) into scripts/validate/ to reduce clutter in
the root scripts/ directory. Update all references across Makefile,
CI/CD configs, pre-commit hooks, docs (~40 files), and Python imports.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-09 21:35:53 +01:00
parent d201221fb1
commit 7a9dda282d
63 changed files with 173 additions and 174 deletions

View File

@@ -0,0 +1,85 @@
# scripts/backup_database.py
"""Database backup utility that uses project configuration."""
import os
import sqlite3
import sys
from datetime import datetime
from pathlib import Path
# Add project root to Python path
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from app.core.config import settings
def get_database_path():
"""Extract database path from DATABASE_URL."""
db_url = settings.database_url
if db_url.startswith("sqlite:///"):
# Remove sqlite:/// prefix and handle relative paths
db_path = db_url.replace("sqlite:///", "")
if db_path.startswith("./"):
db_path = db_path[2:] # Remove ./ prefix
return db_path
# For PostgreSQL or other databases, we can't do file backup
print(f"[INFO] Database type: {db_url.split('://')[0]}")
print("[ERROR] File backup only supported for SQLite databases")
return None
def backup_sqlite_database():
"""Backup SQLite database using proper connection."""
db_path = get_database_path()
if not db_path:
return False
if not os.path.exists(db_path):
print(f"[INFO] No database file found at: {db_path}")
print("[INFO] Nothing to backup")
return True
# Create backup directory
backup_dir = Path("backups")
backup_dir.mkdir(exist_ok=True)
# Create timestamped backup
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_path = backup_dir / f"database_backup_{timestamp}.db"
try:
# Use SQLite backup API for better consistency
source_conn = sqlite3.connect(db_path)
backup_conn = sqlite3.connect(str(backup_path))
source_conn.backup(backup_conn)
source_conn.close()
backup_conn.close()
print(f"[SUCCESS] Database backed up to: {backup_path}")
return True
except Exception as e:
print(f"[ERROR] Backup failed: {e}")
return False
def backup_database():
"""Main backup function that handles different database types."""
print("[BACKUP] Starting database backup...")
print(f"[INFO] Database URL: {settings.database_url}")
if settings.database_url.startswith("sqlite"):
return backup_sqlite_database()
print("[INFO] For PostgreSQL databases, use pg_dump:")
print(f"pg_dump {settings.database_url} > backup_$(date +%Y%m%d_%H%M%S).sql")
return True
if __name__ == "__main__":
success = backup_database()
sys.exit(0 if success else 1)

View File

@@ -0,0 +1,586 @@
#!/usr/bin/env python3
"""
Create Default Platform Content Pages (CMS)
This script creates platform-level default content pages that all stores inherit.
These pages serve as the baseline content for:
- About Us
- Contact
- FAQ
- Shipping Policy
- Return Policy
- Privacy Policy
- Terms of Service
Stores can override any of these pages with their own custom content.
Prerequisites:
- Database migrations must be applied
- content_pages table must exist
Usage:
python scripts/create_default_content_pages.py
# Or with make:
make create-cms-defaults
"""
import sys
from datetime import UTC, datetime
from pathlib import Path
# Add project root to path
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
from sqlalchemy import select
from sqlalchemy.orm import Session
# Register all models with SQLAlchemy so string-based relationships resolve
for _mod in [
"app.modules.billing.models",
"app.modules.inventory.models",
"app.modules.cart.models",
"app.modules.messaging.models",
"app.modules.loyalty.models",
"app.modules.catalog.models",
"app.modules.customers.models",
"app.modules.orders.models",
"app.modules.marketplace.models",
"app.modules.cms.models",
]:
try:
__import__(_mod)
except ImportError:
pass
from app.core.database import SessionLocal
from app.modules.cms.models import ContentPage
from app.modules.tenancy.models import Platform
# ============================================================================
# DEFAULT PAGE CONTENT
# ============================================================================
DEFAULT_PAGES = [
{
"slug": "about",
"title": "About Us",
"content": """
<div class="prose-content">
<h2>Welcome to Our Platform</h2>
<p>We are a multi-store e-commerce platform connecting quality sellers with customers worldwide.</p>
<h3>Our Mission</h3>
<p>To empower independent businesses and artisans by providing them with the tools and platform they need to reach customers globally.</p>
<h3>What We Offer</h3>
<ul>
<li>Curated selection of quality products from verified stores</li>
<li>Secure payment processing and buyer protection</li>
<li>Fast and reliable shipping options</li>
<li>Dedicated customer support</li>
</ul>
<h3>Our Values</h3>
<ul>
<li><strong>Quality:</strong> We work only with stores who meet our quality standards</li>
<li><strong>Transparency:</strong> Clear pricing, policies, and communication</li>
<li><strong>Customer First:</strong> Your satisfaction is our priority</li>
<li><strong>Innovation:</strong> Continuously improving our platform and services</li>
</ul>
</div>
""",
"meta_description": "Learn about our mission to connect quality stores with customers worldwide",
"meta_keywords": "about us, mission, values, platform",
"show_in_footer": True,
"show_in_header": True,
"display_order": 1,
},
{
"slug": "contact",
"title": "Contact Us",
"content": """
<div class="prose-content">
<h2>Get in Touch</h2>
<p>We're here to help! Reach out to us through any of the following channels:</p>
<h3>Customer Support</h3>
<p>For questions about orders, products, or general inquiries:</p>
<ul>
<li><strong>Email:</strong> support@example.com</li>
<li><strong>Phone:</strong> 1-800-EXAMPLE (Mon-Fri, 9am-6pm EST)</li>
<li><strong>Live Chat:</strong> Available on our website during business hours</li>
</ul>
<h3>Business Inquiries</h3>
<p>Interested in becoming a store or partnering with us?</p>
<ul>
<li><strong>Email:</strong> stores@example.com</li>
</ul>
<h3>Office Address</h3>
<p>
123 Commerce Street<br>
Suite 456<br>
City, State 12345<br>
United States
</p>
<h3>Response Time</h3>
<p>We typically respond to all inquiries within 24 hours during business days.</p>
</div>
""",
"meta_description": "Contact our customer support team for assistance with orders, products, or inquiries",
"meta_keywords": "contact, support, customer service, help",
"show_in_footer": True,
"show_in_header": True,
"display_order": 2,
},
{
"slug": "faq",
"title": "Frequently Asked Questions",
"content": """
<div class="prose-content">
<h2>Frequently Asked Questions</h2>
<h3>Orders & Shipping</h3>
<h4>How do I track my order?</h4>
<p>Once your order ships, you'll receive a tracking number via email. You can also view your order status in your account dashboard.</p>
<h4>How long does shipping take?</h4>
<p>Shipping times vary by store and destination. Most orders arrive within 3-7 business days. See our <a href="/shipping">Shipping Policy</a> for details.</p>
<h4>Do you ship internationally?</h4>
<p>Yes! We ship to most countries worldwide. International shipping times and costs vary by destination.</p>
<h3>Returns & Refunds</h3>
<h4>What is your return policy?</h4>
<p>Most items can be returned within 30 days of delivery. See our <a href="/returns">Return Policy</a> for complete details.</p>
<h4>How do I request a refund?</h4>
<p>Contact the store directly through your order page or reach out to our support team for assistance.</p>
<h3>Payments</h3>
<h4>What payment methods do you accept?</h4>
<p>We accept all major credit cards, PayPal, and other secure payment methods.</p>
<h4>Is my payment information secure?</h4>
<p>Yes! We use industry-standard encryption and never store your full payment details.</p>
<h3>Account</h3>
<h4>Do I need an account to make a purchase?</h4>
<p>No, you can checkout as a guest. However, creating an account lets you track orders and save your preferences.</p>
<h4>How do I reset my password?</h4>
<p>Click "Forgot Password" on the login page and follow the instructions sent to your email.</p>
</div>
""",
"meta_description": "Find answers to common questions about orders, shipping, returns, and more",
"meta_keywords": "faq, questions, help, support",
"show_in_footer": True,
"show_in_header": False,
"display_order": 3,
},
{
"slug": "shipping",
"title": "Shipping Policy",
"content": """
<div class="prose-content">
<h2>Shipping Policy</h2>
<h3>Shipping Methods</h3>
<p>We offer multiple shipping options to meet your needs:</p>
<ul>
<li><strong>Standard Shipping:</strong> 5-7 business days</li>
<li><strong>Express Shipping:</strong> 2-3 business days</li>
<li><strong>Overnight Shipping:</strong> Next business day</li>
</ul>
<h3>Shipping Costs</h3>
<p>Shipping costs are calculated based on:</p>
<ul>
<li>Package weight and dimensions</li>
<li>Destination address</li>
<li>Selected shipping method</li>
</ul>
<p>Free standard shipping on orders over $50!</p>
<h3>Processing Time</h3>
<p>Orders are typically processed within 1-2 business days. You'll receive a confirmation email when your order ships with tracking information.</p>
<h3>International Shipping</h3>
<p>We ship to most countries worldwide. International shipping times vary by destination (typically 7-21 business days).</p>
<ul>
<li>Customs fees and import duties are the responsibility of the recipient</li>
<li>International orders may be subject to customs inspection</li>
</ul>
<h3>Tracking</h3>
<p>All orders include tracking. You can monitor your package status through:</p>
<ul>
<li>Your account dashboard</li>
<li>Email notifications</li>
<li>Carrier tracking website</li>
</ul>
<h3>Delivery Issues</h3>
<p>If you experience any delivery issues, please contact us within 7 days of the expected delivery date.</p>
</div>
""",
"meta_description": "Learn about our shipping methods, costs, and delivery times",
"meta_keywords": "shipping, delivery, tracking, international",
"show_in_footer": True,
"show_in_header": False,
"display_order": 4,
},
{
"slug": "returns",
"title": "Return & Refund Policy",
"content": """
<div class="prose-content">
<h2>Return & Refund Policy</h2>
<h3>30-Day Return Window</h3>
<p>Most items can be returned within 30 days of delivery for a full refund or exchange.</p>
<h3>Return Requirements</h3>
<p>To be eligible for a return, items must:</p>
<ul>
<li>Be in original condition with tags attached</li>
<li>Include original packaging and accessories</li>
<li>Not be used or damaged</li>
<li>Include proof of purchase</li>
</ul>
<h3>Non-Returnable Items</h3>
<p>The following items cannot be returned:</p>
<ul>
<li>Personalized or custom-made items</li>
<li>Perishable goods</li>
<li>Health and personal care items</li>
<li>Digital downloads</li>
<li>Sale or clearance items (marked as final sale)</li>
</ul>
<h3>How to Return</h3>
<ol>
<li>Log into your account and go to "Order History"</li>
<li>Select the order and click "Request Return"</li>
<li>Choose items and provide reason for return</li>
<li>Print the prepaid return label</li>
<li>Pack items securely and attach label</li>
<li>Drop off at any authorized carrier location</li>
</ol>
<h3>Refund Process</h3>
<p>Once we receive your return:</p>
<ul>
<li>We'll inspect the items (2-3 business days)</li>
<li>Approved refunds are processed within 5-7 business days</li>
<li>Refunds go to original payment method</li>
<li>You'll receive email confirmation</li>
</ul>
<h3>Return Shipping</h3>
<p>Return shipping is free for defective or incorrect items. For other returns, a $5.99 return shipping fee will be deducted from your refund.</p>
<h3>Exchanges</h3>
<p>We offer free exchanges for different sizes or colors of the same item. Contact support to arrange an exchange.</p>
<h3>Damaged or Defective Items</h3>
<p>If you receive a damaged or defective item, please contact us within 7 days of delivery with photos. We'll send a replacement or full refund immediately.</p>
</div>
""",
"meta_description": "Our 30-day return policy ensures your satisfaction with every purchase",
"meta_keywords": "returns, refunds, exchange, policy",
"show_in_footer": True,
"show_in_header": False,
"display_order": 5,
},
{
"slug": "privacy",
"title": "Privacy Policy",
"content": """
<div class="prose-content">
<h2>Privacy Policy</h2>
<p><em>Last Updated: [Date]</em></p>
<h3>Information We Collect</h3>
<p>We collect information you provide directly:</p>
<ul>
<li>Name, email address, and contact information</li>
<li>Shipping and billing addresses</li>
<li>Payment information (processed securely by our payment processor)</li>
<li>Order history and preferences</li>
</ul>
<p>We automatically collect:</p>
<ul>
<li>Browser type and device information</li>
<li>IP address and location data</li>
<li>Cookies and similar tracking technologies</li>
<li>Usage data and analytics</li>
</ul>
<h3>How We Use Your Information</h3>
<p>We use your information to:</p>
<ul>
<li>Process and fulfill your orders</li>
<li>Communicate about orders and account</li>
<li>Provide customer support</li>
<li>Send marketing communications (with your consent)</li>
<li>Improve our platform and services</li>
<li>Prevent fraud and enhance security</li>
</ul>
<h3>Information Sharing</h3>
<p>We share your information only when necessary:</p>
<ul>
<li><strong>Stores:</strong> To fulfill your orders</li>
<li><strong>Service Providers:</strong> Payment processors, shipping carriers, analytics</li>
<li><strong>Legal Requirements:</strong> When required by law</li>
</ul>
<p>We never sell your personal information to third parties.</p>
<h3>Your Rights</h3>
<p>You have the right to:</p>
<ul>
<li>Access your personal data</li>
<li>Correct inaccurate information</li>
<li>Request deletion of your data</li>
<li>Opt-out of marketing communications</li>
<li>Export your data</li>
</ul>
<h3>Data Security</h3>
<p>We implement industry-standard security measures including:</p>
<ul>
<li>SSL/TLS encryption for data transmission</li>
<li>Secure password hashing</li>
<li>Regular security audits</li>
<li>Limited employee access to personal data</li>
</ul>
<h3>Cookies</h3>
<p>We use cookies to enhance your experience. You can control cookies through your browser settings.</p>
<h3>Children's Privacy</h3>
<p>Our platform is not intended for children under 13. We do not knowingly collect information from children.</p>
<h3>Changes to This Policy</h3>
<p>We may update this policy periodically. Significant changes will be communicated via email.</p>
<h3>Contact Us</h3>
<p>For privacy-related questions, contact us at privacy@example.com</p>
</div>
""",
"meta_description": "Learn how we collect, use, and protect your personal information",
"meta_keywords": "privacy, data protection, security, policy",
"show_in_footer": False,
"show_in_header": False,
"show_in_legal": True,
"display_order": 6,
},
{
"slug": "terms",
"title": "Terms of Service",
"content": """
<div class="prose-content">
<h2>Terms of Service</h2>
<p><em>Last Updated: [Date]</em></p>
<h3>1. Acceptance of Terms</h3>
<p>By accessing and using this platform, you accept and agree to be bound by these Terms of Service.</p>
<h3>2. Account Registration</h3>
<p>You must:</p>
<ul>
<li>Be at least 18 years old</li>
<li>Provide accurate and complete information</li>
<li>Maintain the security of your account</li>
<li>Notify us of unauthorized access</li>
</ul>
<h3>3. User Conduct</h3>
<p>You agree not to:</p>
<ul>
<li>Violate any laws or regulations</li>
<li>Infringe on intellectual property rights</li>
<li>Transmit harmful code or viruses</li>
<li>Harass or harm other users</li>
<li>Engage in fraudulent activities</li>
<li>Scrape or data mine our platform</li>
</ul>
<h3>4. Product Listings</h3>
<p>Product information is provided by stores. While we strive for accuracy:</p>
<ul>
<li>Descriptions may contain errors</li>
<li>Prices are subject to change</li>
<li>Availability is not guaranteed</li>
<li>Images are representative</li>
</ul>
<h3>5. Orders and Payment</h3>
<ul>
<li>All orders are subject to acceptance</li>
<li>We reserve the right to refuse or cancel orders</li>
<li>Prices include applicable taxes unless stated otherwise</li>
<li>Payment must be received before order processing</li>
</ul>
<h3>6. Intellectual Property</h3>
<p>All content on this platform is protected by copyright, trademark, and other laws. You may not:</p>
<ul>
<li>Reproduce or distribute our content</li>
<li>Create derivative works</li>
<li>Use our trademarks without permission</li>
</ul>
<h3>7. Disclaimer of Warranties</h3>
<p>This platform is provided "as is" without warranties of any kind, express or implied.</p>
<h3>8. Limitation of Liability</h3>
<p>We are not liable for indirect, incidental, or consequential damages arising from your use of the platform.</p>
<h3>9. Indemnification</h3>
<p>You agree to indemnify and hold us harmless from claims arising from your use of the platform or violation of these terms.</p>
<h3>10. Dispute Resolution</h3>
<p>Disputes will be resolved through binding arbitration in accordance with the rules of the American Arbitration Association.</p>
<h3>11. Governing Law</h3>
<p>These terms are governed by the laws of [State/Country], without regard to conflict of law principles.</p>
<h3>12. Changes to Terms</h3>
<p>We may modify these terms at any time. Continued use constitutes acceptance of modified terms.</p>
<h3>13. Contact</h3>
<p>For questions about these terms, contact legal@example.com</p>
</div>
""",
"meta_description": "Read our terms of service governing the use of our platform",
"meta_keywords": "terms, conditions, legal, agreement",
"show_in_footer": False,
"show_in_header": False,
"show_in_legal": True,
"display_order": 7,
},
]
# ============================================================================
# SCRIPT FUNCTIONS
# ============================================================================
def create_default_pages(db: Session) -> None:
"""
Create default platform content pages.
This function is idempotent - it will skip pages that already exist.
"""
print("\n" + "=" * 70)
print("Creating Default Platform Content Pages (CMS)")
print("=" * 70 + "\n")
# Resolve OMS platform for platform_id
oms_platform = db.execute(
select(Platform).where(Platform.code == "oms")
).scalar_one_or_none()
if not oms_platform:
print(" ⚠️ OMS platform not found. Run init_production.py first.")
return
platform_id = oms_platform.id
created_count = 0
skipped_count = 0
for page_data in DEFAULT_PAGES:
# Check if page already exists (platform default with this slug)
existing = db.execute(
select(ContentPage).where(
ContentPage.store_id == None, ContentPage.slug == page_data["slug"]
)
).scalar_one_or_none()
if existing:
print(
f" ⏭️ Skipped: {page_data['title']} (/{page_data['slug']}) - already exists"
)
skipped_count += 1
continue
# Create new platform default page
page = ContentPage(
platform_id=platform_id,
store_id=None, # Platform default
slug=page_data["slug"],
title=page_data["title"],
content=page_data["content"],
content_format="html",
meta_description=page_data["meta_description"],
meta_keywords=page_data["meta_keywords"],
is_published=True,
published_at=datetime.now(UTC),
show_in_footer=page_data.get("show_in_footer", True),
show_in_header=page_data.get("show_in_header", False),
show_in_legal=page_data.get("show_in_legal", False),
display_order=page_data["display_order"],
created_at=datetime.now(UTC),
updated_at=datetime.now(UTC),
)
db.add(page)
print(f" ✅ Created: {page_data['title']} (/{page_data['slug']})")
created_count += 1
db.commit()
print("\n" + "=" * 70)
print("Summary:")
print(f" Created: {created_count} pages")
print(f" Skipped: {skipped_count} pages (already exist)")
print(f" Total: {created_count + skipped_count} pages")
print("=" * 70 + "\n")
if created_count > 0:
print("✅ Default platform content pages created successfully!\n")
print("Next steps:")
print(
" 1. View pages at: /about, /contact, /faq, /shipping, /returns, /privacy, /terms"
)
print(" 2. Stores can override these pages through the store dashboard")
print(" 3. Edit platform defaults through the admin panel\n")
else:
print(" All default pages already exist. No changes made.\n")
# ============================================================================
# MAIN EXECUTION
# ============================================================================
def main():
"""Main execution function."""
print("\n🚀 Starting Default Content Pages Creation Script...\n")
db = SessionLocal()
try:
create_default_pages(db)
print("✅ Script completed successfully!\n")
except Exception as e:
print(f"\n❌ Error: {e}\n")
db.rollback()
raise
finally:
db.close()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,541 @@
#!/usr/bin/env python3
"""
Create Platform Content Pages
This script creates default platform-level content pages:
- Platform Homepage (slug='platform_homepage')
- About Us (slug='about')
- FAQ (slug='faq')
- Terms of Service (slug='terms')
- Privacy Policy (slug='privacy')
- Contact Us (slug='contact')
All pages are created with store_id=NULL (platform-level defaults).
Usage:
python scripts/create_platform_pages.py
"""
import sys
from pathlib import Path
# Add project root to path
project_root = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(project_root))
# Register all models with SQLAlchemy so string-based relationships resolve
for _mod in [
"app.modules.billing.models",
"app.modules.inventory.models",
"app.modules.cart.models",
"app.modules.messaging.models",
"app.modules.loyalty.models",
"app.modules.catalog.models",
"app.modules.customers.models",
"app.modules.orders.models",
"app.modules.marketplace.models",
"app.modules.cms.models",
]:
try:
__import__(_mod)
except ImportError:
pass
from sqlalchemy import select
from app.core.database import SessionLocal
from app.modules.cms.services import content_page_service
from app.modules.tenancy.models import Platform
def create_platform_pages():
"""Create default platform content pages."""
db = SessionLocal()
try:
print("=" * 80)
print("CREATING PLATFORM CONTENT PAGES")
print("=" * 80)
print()
# Import ContentPage for checking existing pages
from app.modules.cms.models import ContentPage
# Resolve OMS platform
oms_platform = db.execute(
select(Platform).where(Platform.code == "oms")
).scalar_one_or_none()
if not oms_platform:
print(" ⚠️ OMS platform not found. Run init_production.py first.")
return
platform_id = oms_platform.id
# ========================================================================
# 1. PLATFORM HOMEPAGE
# ========================================================================
print("1. Creating Platform Homepage...")
# Check if already exists
existing = (
db.query(ContentPage)
.filter_by(store_id=None, slug="platform_homepage")
.first()
)
if existing:
print(
f" ⚠️ Skipped: Platform Homepage - already exists (ID: {existing.id})"
)
else:
try:
homepage = content_page_service.create_page(
db,
platform_id=platform_id,
slug="platform_homepage",
title="Welcome to Our Multi-Store Marketplace",
content="""
<p class="lead">
Connect stores with customers worldwide. Build your online store and reach millions of shoppers.
</p>
<p>
Our platform empowers entrepreneurs to launch their own branded e-commerce stores
with minimal effort and maximum impact.
</p>
""",
template="modern", # Uses platform/homepage-modern.html
store_id=None, # Platform-level page
is_published=True,
show_in_header=False, # Homepage is not in menu (it's the root)
show_in_footer=False,
display_order=0,
meta_description="Leading multi-store marketplace platform. Connect with thousands of stores and discover millions of products.",
meta_keywords="marketplace, multi-store, e-commerce, online shopping, platform",
)
print(f" ✅ Created: {homepage.title} (/{homepage.slug})")
except Exception as e:
print(f" ⚠️ Error: Platform Homepage - {str(e)}")
# ========================================================================
# 2. ABOUT US
# ========================================================================
print("2. Creating About Us page...")
existing = db.query(ContentPage).filter_by(store_id=None, slug="about").first()
if existing:
print(f" ⚠️ Skipped: About Us - already exists (ID: {existing.id})")
else:
try:
about = content_page_service.create_page(
db,
platform_id=platform_id,
slug="about",
title="About Us",
content="""
<h2>Our Mission</h2>
<p>
We're on a mission to democratize e-commerce by providing powerful,
easy-to-use tools for entrepreneurs worldwide.
</p>
<h2>Our Story</h2>
<p>
Founded in 2024, our platform has grown to serve over 10,000 active stores
and millions of customers around the globe. We believe that everyone should
have the opportunity to build and grow their own online business.
</p>
<h2>Why Choose Us?</h2>
<ul>
<li><strong>Easy to Start:</strong> Launch your store in minutes, not months</li>
<li><strong>Powerful Tools:</strong> Everything you need to succeed in one platform</li>
<li><strong>Scalable:</strong> Grow from startup to enterprise seamlessly</li>
<li><strong>Reliable:</strong> 99.9% uptime guarantee with 24/7 support</li>
</ul>
<h2>Our Values</h2>
<ul>
<li><strong>Innovation:</strong> We constantly improve and evolve our platform</li>
<li><strong>Transparency:</strong> No hidden fees, no surprises</li>
<li><strong>Community:</strong> We succeed when our stores succeed</li>
<li><strong>Excellence:</strong> We strive for the highest quality in everything we do</li>
</ul>
""",
store_id=None,
is_published=True,
show_in_header=True, # Show in header navigation
show_in_footer=True, # Show in footer
display_order=1,
meta_description="Learn about our mission to democratize e-commerce and empower entrepreneurs worldwide.",
meta_keywords="about us, mission, vision, values, merchant",
)
print(f" ✅ Created: {about.title} (/{about.slug})")
except Exception as e:
print(f" ⚠️ Error: About Us - {str(e)}")
# ========================================================================
# 3. FAQ
# ========================================================================
print("3. Creating FAQ page...")
existing = db.query(ContentPage).filter_by(store_id=None, slug="faq").first()
if existing:
print(f" ⚠️ Skipped: FAQ - already exists (ID: {existing.id})")
else:
try:
faq = content_page_service.create_page(
db,
platform_id=platform_id,
slug="faq",
title="Frequently Asked Questions",
content="""
<h2>Getting Started</h2>
<h3>How do I create a store account?</h3>
<p>
Contact our sales team to get started. We'll set up your account and provide
you with everything you need to launch your store.
</p>
<h3>How long does it take to set up my store?</h3>
<p>
Most stores can launch their store in less than 24 hours. Our team will guide
you through the setup process step by step.
</p>
<h2>Pricing & Payment</h2>
<h3>What are your pricing plans?</h3>
<p>
We offer flexible pricing plans based on your business needs. Contact us for
detailed pricing information and to find the plan that's right for you.
</p>
<h3>When do I get paid?</h3>
<p>
Payments are processed weekly, with funds typically reaching your account
within 2-3 business days.
</p>
<h2>Features & Support</h2>
<h3>Can I customize my store's appearance?</h3>
<p>
Yes! Our platform supports full theme customization including colors, fonts,
logos, and layouts. Make your store truly yours.
</p>
<h3>What kind of support do you provide?</h3>
<p>
We offer 24/7 email support for all stores, with priority phone support
available for enterprise plans.
</p>
<h2>Technical Questions</h2>
<h3>Do I need technical knowledge to use the platform?</h3>
<p>
No! Our platform is designed to be user-friendly for everyone. However, if you
want to customize advanced features, our documentation and support team are here to help.
</p>
<h3>Can I integrate with other tools?</h3>
<p>
Yes, we support integrations with popular payment gateways, shipping providers,
and marketing tools.
</p>
""",
store_id=None,
is_published=True,
show_in_header=True, # Show in header navigation
show_in_footer=True,
display_order=2,
meta_description="Find answers to common questions about our marketplace platform.",
meta_keywords="faq, frequently asked questions, help, support",
)
print(f" ✅ Created: {faq.title} (/{faq.slug})")
except Exception as e:
print(f" ⚠️ Error: FAQ - {str(e)}")
# ========================================================================
# 4. CONTACT US
# ========================================================================
print("4. Creating Contact Us page...")
existing = (
db.query(ContentPage).filter_by(store_id=None, slug="contact").first()
)
if existing:
print(f" ⚠️ Skipped: Contact Us - already exists (ID: {existing.id})")
else:
try:
contact = content_page_service.create_page(
db,
platform_id=platform_id,
slug="contact",
title="Contact Us",
content="""
<h2>Get in Touch</h2>
<p>
We'd love to hear from you! Whether you have questions about our platform,
need technical support, or want to discuss partnership opportunities, our
team is here to help.
</p>
<h2>Contact Information</h2>
<ul>
<li><strong>Email:</strong> support@marketplace.com</li>
<li><strong>Phone:</strong> +1 (555) 123-4567</li>
<li><strong>Hours:</strong> Monday - Friday, 9 AM - 6 PM PST</li>
</ul>
<h2>Office Address</h2>
<p>
123 Business Street, Suite 100<br>
San Francisco, CA 94102<br>
United States
</p>
<h2>Sales Inquiries</h2>
<p>
Interested in launching your store on our platform?<br>
Email: <a href="mailto:sales@marketplace.com">sales@marketplace.com</a>
</p>
<h2>Technical Support</h2>
<p>
Need help with your store?<br>
Email: <a href="mailto:support@marketplace.com">support@marketplace.com</a><br>
24/7 email support for all stores
</p>
<h2>Media & Press</h2>
<p>
For media inquiries and press releases:<br>
Email: <a href="mailto:press@marketplace.com">press@marketplace.com</a>
</p>
""",
store_id=None,
is_published=True,
show_in_header=True, # Show in header navigation
show_in_footer=True,
display_order=3,
meta_description="Get in touch with our team. We're here to help you succeed.",
meta_keywords="contact, support, email, phone, address",
)
print(f" ✅ Created: {contact.title} (/{contact.slug})")
except Exception as e:
print(f" ⚠️ Error: Contact Us - {str(e)}")
# ========================================================================
# 5. TERMS OF SERVICE
# ========================================================================
print("5. Creating Terms of Service page...")
existing = db.query(ContentPage).filter_by(store_id=None, slug="terms").first()
if existing:
print(
f" ⚠️ Skipped: Terms of Service - already exists (ID: {existing.id})"
)
else:
try:
terms = content_page_service.create_page(
db,
platform_id=platform_id,
slug="terms",
title="Terms of Service",
content="""
<p><em>Last updated: January 1, 2024</em></p>
<h2>1. Acceptance of Terms</h2>
<p>
By accessing and using this marketplace platform, you accept and agree to be
bound by the terms and provisions of this agreement.
</p>
<h2>2. Use License</h2>
<p>
Permission is granted to temporarily access the materials on our platform for
personal, non-commercial transitory viewing only.
</p>
<h2>3. Account Terms</h2>
<ul>
<li>You must be at least 18 years old to use our platform</li>
<li>You must provide accurate and complete registration information</li>
<li>You are responsible for maintaining the security of your account</li>
<li>You are responsible for all activities under your account</li>
</ul>
<h2>4. Store Responsibilities</h2>
<ul>
<li>Provide accurate product information and pricing</li>
<li>Honor all orders and commitments made through the platform</li>
<li>Comply with all applicable laws and regulations</li>
<li>Maintain appropriate customer service standards</li>
</ul>
<h2>5. Prohibited Activities</h2>
<p>You may not use our platform to:</p>
<ul>
<li>Engage in any fraudulent or illegal activities</li>
<li>Violate any intellectual property rights</li>
<li>Transmit harmful code or malware</li>
<li>Interfere with platform operations</li>
</ul>
<h2>6. Termination</h2>
<p>
We reserve the right to terminate or suspend your account at any time for
violation of these terms.
</p>
<h2>7. Limitation of Liability</h2>
<p>
In no event shall our merchant be liable for any damages arising out of the
use or inability to use our platform.
</p>
<h2>8. Changes to Terms</h2>
<p>
We reserve the right to modify these terms at any time. We will notify users
of any changes via email.
</p>
<h2>9. Contact</h2>
<p>
If you have any questions about these Terms, please contact us at
<a href="mailto:legal@marketplace.com">legal@marketplace.com</a>.
</p>
""",
store_id=None,
is_published=True,
show_in_header=False, # Too legal for header
show_in_footer=True, # Show in footer
display_order=10,
meta_description="Read our terms of service and platform usage policies.",
meta_keywords="terms of service, terms, legal, policy, agreement",
)
print(f" ✅ Created: {terms.title} (/{terms.slug})")
except Exception as e:
print(f" ⚠️ Error: Terms of Service - {str(e)}")
# ========================================================================
# 6. PRIVACY POLICY
# ========================================================================
print("6. Creating Privacy Policy page...")
existing = (
db.query(ContentPage).filter_by(store_id=None, slug="privacy").first()
)
if existing:
print(f" ⚠️ Skipped: Privacy Policy - already exists (ID: {existing.id})")
else:
try:
privacy = content_page_service.create_page(
db,
platform_id=platform_id,
slug="privacy",
title="Privacy Policy",
content="""
<p><em>Last updated: January 1, 2024</em></p>
<h2>1. Information We Collect</h2>
<p>We collect information you provide directly to us, including:</p>
<ul>
<li>Name, email address, and contact information</li>
<li>Payment and billing information</li>
<li>Store and product information</li>
<li>Communications with us</li>
</ul>
<h2>2. How We Use Your Information</h2>
<p>We use the information we collect to:</p>
<ul>
<li>Provide, maintain, and improve our services</li>
<li>Process transactions and send related information</li>
<li>Send technical notices and support messages</li>
<li>Respond to your comments and questions</li>
<li>Monitor and analyze trends and usage</li>
</ul>
<h2>3. Information Sharing</h2>
<p>
We do not sell your personal information. We may share information with:
</p>
<ul>
<li>Service providers who help us operate our platform</li>
<li>Law enforcement when required by law</li>
<li>Other parties with your consent</li>
</ul>
<h2>4. Data Security</h2>
<p>
We implement appropriate security measures to protect your personal information.
However, no method of transmission over the internet is 100% secure.
</p>
<h2>5. Your Rights</h2>
<p>You have the right to:</p>
<ul>
<li>Access your personal information</li>
<li>Correct inaccurate information</li>
<li>Request deletion of your information</li>
<li>Opt-out of marketing communications</li>
</ul>
<h2>6. Cookies</h2>
<p>
We use cookies and similar tracking technologies to track activity on our
platform and hold certain information. You can instruct your browser to
refuse cookies.
</p>
<h2>7. Changes to This Policy</h2>
<p>
We may update this privacy policy from time to time. We will notify you of
any changes by posting the new policy on this page.
</p>
<h2>8. Contact Us</h2>
<p>
If you have questions about this Privacy Policy, please contact us at
<a href="mailto:privacy@marketplace.com">privacy@marketplace.com</a>.
</p>
""",
store_id=None,
is_published=True,
show_in_header=False, # Too legal for header
show_in_footer=True, # Show in footer
display_order=11,
meta_description="Learn how we collect, use, and protect your personal information.",
meta_keywords="privacy policy, privacy, data protection, gdpr, personal information",
)
print(f" ✅ Created: {privacy.title} (/{privacy.slug})")
except Exception as e:
print(f" ⚠️ Error: Privacy Policy - {str(e)}")
db.commit()
print()
print("=" * 80)
print("✅ Platform pages creation completed successfully!")
print("=" * 80)
print()
print("Created pages:")
print(" - Platform Homepage: http://localhost:8000/")
print(" - About Us: http://localhost:8000/about")
print(" - FAQ: http://localhost:8000/faq")
print(" - Contact Us: http://localhost:8000/contact")
print(" - Terms of Service: http://localhost:8000/terms")
print(" - Privacy Policy: http://localhost:8000/privacy")
print()
except Exception as e:
print(f"\n❌ Error: {e}")
db.rollback()
raise
finally:
db.close()
if __name__ == "__main__":
create_platform_pages()

0
scripts/seed/init_db.py Normal file
View File

View File

@@ -0,0 +1,137 @@
#!/usr/bin/env python3
"""
Initialize default log settings in database.
Run this script to create default logging configuration settings.
"""
# Register all models with SQLAlchemy so string-based relationships resolve
for _mod in [
"app.modules.billing.models",
"app.modules.inventory.models",
"app.modules.cart.models",
"app.modules.messaging.models",
"app.modules.loyalty.models",
"app.modules.catalog.models",
"app.modules.customers.models",
"app.modules.orders.models",
"app.modules.marketplace.models",
"app.modules.cms.models",
]:
try:
__import__(_mod)
except ImportError:
pass
from app.core.database import SessionLocal
from app.modules.tenancy.models import AdminSetting
def init_log_settings():
"""Create default log settings if they don't exist."""
db = SessionLocal()
try:
settings_to_create = [
{
"key": "log_level",
"value": "INFO",
"value_type": "string",
"category": "logging",
"description": "Application log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
"is_public": False,
"is_encrypted": False,
},
{
"key": "log_file_max_size_mb",
"value": "10",
"value_type": "integer",
"category": "logging",
"description": "Maximum log file size in MB before rotation",
"is_public": False,
"is_encrypted": False,
},
{
"key": "log_file_backup_count",
"value": "5",
"value_type": "integer",
"category": "logging",
"description": "Number of rotated log files to keep",
"is_public": False,
"is_encrypted": False,
},
{
"key": "db_log_retention_days",
"value": "30",
"value_type": "integer",
"category": "logging",
"description": "Number of days to retain logs in database",
"is_public": False,
"is_encrypted": False,
},
{
"key": "file_logging_enabled",
"value": "true",
"value_type": "boolean",
"category": "logging",
"description": "Enable file-based logging",
"is_public": False,
"is_encrypted": False,
},
{
"key": "db_logging_enabled",
"value": "true",
"value_type": "boolean",
"category": "logging",
"description": "Enable database logging for critical events",
"is_public": False,
"is_encrypted": False,
},
]
created_count = 0
updated_count = 0
for setting_data in settings_to_create:
existing = (
db.query(AdminSetting)
.filter(AdminSetting.key == setting_data["key"])
.first()
)
if existing:
print(
f"✓ Setting '{setting_data['key']}' already exists (value: {existing.value})"
)
updated_count += 1
else:
setting = AdminSetting(**setting_data)
db.add(setting)
created_count += 1
print(
f"✓ Created setting '{setting_data['key']}' = {setting_data['value']}"
)
db.commit()
print("\n" + "=" * 70)
print("LOG SETTINGS INITIALIZATION COMPLETE")
print("=" * 70)
print(f" Created: {created_count} settings")
print(f" Existing: {updated_count} settings")
print(f" Total: {len(settings_to_create)} settings")
print("=" * 70)
except Exception as e:
db.rollback()
print(f"Error initializing log settings: {e}")
raise
finally:
db.close()
if __name__ == "__main__":
print("=" * 70)
print("INITIALIZING LOG SETTINGS")
print("=" * 70)
init_log_settings()

View File

@@ -0,0 +1,649 @@
#!/usr/bin/env python3
"""
Production Database Initialization for Wizamart Platform
This script initializes ESSENTIAL data required for production:
- Platform admin user
- Default store roles and permissions
- Admin settings
- Platform configuration
This is SAFE to run in production and should be run after migrations.
Usage:
make init-prod
This script is idempotent - safe to run multiple times.
"""
import sys
from datetime import UTC, datetime
from pathlib import Path
# Add project root to path
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.core.config import (
print_environment_info,
settings,
validate_production_settings,
)
from app.core.database import SessionLocal
from app.core.environment import is_production
from app.modules.tenancy.services.permission_discovery_service import (
permission_discovery_service,
)
from middleware.auth import AuthManager
from app.modules.tenancy.models import AdminSetting, Platform
from app.modules.tenancy.models import User
from app.modules.billing.models.subscription import SubscriptionTier
# Register all models with SQLAlchemy so string-based relationships resolve
for _mod in [
"app.modules.billing.models",
"app.modules.inventory.models",
"app.modules.cart.models",
"app.modules.messaging.models",
"app.modules.loyalty.models",
"app.modules.catalog.models",
"app.modules.customers.models",
"app.modules.orders.models",
"app.modules.marketplace.models",
"app.modules.cms.models",
]:
try:
__import__(_mod)
except ImportError:
pass
# =============================================================================
# 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}")
# =============================================================================
# INITIALIZATION FUNCTIONS
# =============================================================================
def create_admin_user(db: Session, auth_manager: AuthManager) -> User:
"""Create or get the platform admin user."""
# Check if admin already exists
admin = db.execute(
select(User).where(User.email == settings.admin_email)
).scalar_one_or_none()
if admin:
print_warning(f"Admin user already exists: {admin.email}")
return admin
# Create new admin
hashed_password = auth_manager.hash_password(settings.admin_password)
admin = User(
username=settings.admin_username,
email=settings.admin_email,
hashed_password=hashed_password,
role="admin",
is_super_admin=True,
first_name=settings.admin_first_name,
last_name=settings.admin_last_name,
is_active=True,
is_email_verified=True,
created_at=datetime.now(UTC),
updated_at=datetime.now(UTC),
)
db.add(admin)
db.flush()
print_success(f"Created admin user: {admin.email}")
return admin
def create_default_platforms(db: Session) -> list[Platform]:
"""Create all default platforms (OMS, Main, Loyalty+)."""
platform_defs = [
{
"code": "oms",
"name": "Wizamart OMS",
"description": "Order Management System for multi-store e-commerce",
"domain": settings.platform_domain,
"path_prefix": "oms",
"default_language": "fr",
"supported_languages": ["fr", "de", "en"],
"settings": {},
"theme_config": {},
},
{
"code": "main",
"name": "Wizamart",
"description": "Main marketing site showcasing all Wizamart platforms",
"domain": "wizamart.lu",
"path_prefix": None,
"default_language": "fr",
"supported_languages": ["fr", "de", "en"],
"settings": {"is_marketing_site": True},
"theme_config": {"primary_color": "#2563EB", "secondary_color": "#3B82F6"},
},
{
"code": "loyalty",
"name": "Loyalty+",
"description": "Customer loyalty program platform for Luxembourg businesses",
"domain": "loyalty.lu",
"path_prefix": "loyalty",
"default_language": "fr",
"supported_languages": ["fr", "de", "en"],
"settings": {"features": ["points", "rewards", "tiers", "analytics"]},
"theme_config": {"primary_color": "#8B5CF6", "secondary_color": "#A78BFA"},
},
]
platforms = []
for pdef in platform_defs:
existing = db.execute(
select(Platform).where(Platform.code == pdef["code"])
).scalar_one_or_none()
if existing:
print_warning(f"Platform already exists: {existing.name} ({existing.code})")
platforms.append(existing)
continue
platform = Platform(
code=pdef["code"],
name=pdef["name"],
description=pdef["description"],
domain=pdef["domain"],
path_prefix=pdef["path_prefix"],
default_language=pdef["default_language"],
supported_languages=pdef["supported_languages"],
settings=pdef["settings"],
theme_config=pdef["theme_config"],
is_active=True,
is_public=True,
created_at=datetime.now(UTC),
updated_at=datetime.now(UTC),
)
db.add(platform)
db.flush()
platforms.append(platform)
print_success(f"Created platform: {platform.name} ({platform.code})")
return platforms
def create_default_role_templates(db: Session) -> dict:
"""Create default role templates (not tied to any store).
These serve as templates that can be copied when creating store-specific roles.
Note: Actual roles are store-specific and created when stores are onboarded.
"""
print(" Available role presets:")
print(" - Manager: Nearly full access (except team management)")
print(" - Staff: Day-to-day operations")
print(" - Support: Customer service focused")
print(" - Viewer: Read-only access")
print(" - Marketing: Marketing and customer communication")
print_success("Role templates ready for store onboarding")
return {
"manager": list(permission_discovery_service.get_preset_permissions("manager")),
"staff": list(permission_discovery_service.get_preset_permissions("staff")),
"support": list(permission_discovery_service.get_preset_permissions("support")),
"viewer": list(permission_discovery_service.get_preset_permissions("viewer")),
"marketing": list(permission_discovery_service.get_preset_permissions("marketing")),
}
def create_admin_settings(db: Session) -> int:
"""Create essential admin settings."""
settings_created = 0
# Essential platform settings
default_settings = [
{
"key": "platform_name",
"value": settings.project_name,
"value_type": "string",
"description": "Platform name displayed in admin panel",
"is_public": True,
},
{
"key": "platform_url",
"value": f"https://{settings.platform_domain}",
"value_type": "string",
"description": "Main platform URL",
"is_public": True,
},
{
"key": "support_email",
"value": f"support@{settings.platform_domain}",
"value_type": "string",
"description": "Platform support email",
"is_public": True,
},
{
"key": "max_stores_per_user",
"value": str(settings.max_stores_per_user),
"value_type": "integer",
"description": "Maximum stores a user can own",
"is_public": False,
},
{
"key": "max_team_members_per_store",
"value": str(settings.max_team_members_per_store),
"value_type": "integer",
"description": "Maximum team members per store",
"is_public": False,
},
{
"key": "invitation_expiry_days",
"value": str(settings.invitation_expiry_days),
"value_type": "integer",
"description": "Days until team invitation expires",
"is_public": False,
},
{
"key": "require_store_verification",
"value": "true",
"value_type": "boolean",
"description": "Require admin verification for new stores",
"is_public": False,
},
{
"key": "allow_custom_domains",
"value": str(settings.allow_custom_domains).lower(),
"value_type": "boolean",
"description": "Allow stores to use custom domains",
"is_public": False,
},
{
"key": "maintenance_mode",
"value": "false",
"value_type": "boolean",
"description": "Enable maintenance mode",
"is_public": True,
},
# Logging settings
{
"key": "log_level",
"value": "INFO",
"value_type": "string",
"category": "logging",
"description": "Application log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
"is_public": False,
},
{
"key": "log_file_max_size_mb",
"value": "10",
"value_type": "integer",
"category": "logging",
"description": "Maximum log file size in MB before rotation",
"is_public": False,
},
{
"key": "log_file_backup_count",
"value": "5",
"value_type": "integer",
"category": "logging",
"description": "Number of rotated log files to keep",
"is_public": False,
},
{
"key": "db_log_retention_days",
"value": "30",
"value_type": "integer",
"category": "logging",
"description": "Number of days to retain logs in database",
"is_public": False,
},
{
"key": "file_logging_enabled",
"value": "true",
"value_type": "boolean",
"category": "logging",
"description": "Enable file-based logging",
"is_public": False,
},
{
"key": "db_logging_enabled",
"value": "true",
"value_type": "boolean",
"category": "logging",
"description": "Enable database logging for critical events",
"is_public": False,
},
]
for setting_data in default_settings:
# Check if setting already exists
existing = db.execute(
select(AdminSetting).where(AdminSetting.key == setting_data["key"])
).scalar_one_or_none()
if not existing:
setting = AdminSetting(
key=setting_data["key"],
value=setting_data["value"],
value_type=setting_data["value_type"],
category=setting_data.get("category"),
description=setting_data.get("description"),
is_public=setting_data.get("is_public", False),
created_at=datetime.now(UTC),
updated_at=datetime.now(UTC),
)
db.add(setting)
settings_created += 1
db.flush()
if settings_created > 0:
print_success(f"Created {settings_created} admin settings")
else:
print_warning("Admin settings already exist")
return settings_created
def create_subscription_tiers(db: Session, platform: Platform) -> int:
"""Create default subscription tiers for the OMS platform."""
tier_defs = [
{
"code": "essential",
"name": "Essential",
"price_monthly_cents": 2900,
"price_annual_cents": 29000,
"is_public": True,
"display_order": 10,
},
{
"code": "professional",
"name": "Professional",
"price_monthly_cents": 7900,
"price_annual_cents": 79000,
"is_public": True,
"display_order": 20,
},
{
"code": "business",
"name": "Business",
"price_monthly_cents": 14900,
"price_annual_cents": 149000,
"is_public": True,
"display_order": 30,
},
{
"code": "enterprise",
"name": "Enterprise",
"price_monthly_cents": 29900,
"price_annual_cents": None,
"is_public": False,
"display_order": 40,
},
]
tiers_created = 0
for tdef in tier_defs:
existing = db.execute(
select(SubscriptionTier).where(SubscriptionTier.code == tdef["code"])
).scalar_one_or_none()
if existing:
print_warning(f"Tier already exists: {existing.name} ({existing.code})")
continue
tier = SubscriptionTier(
platform_id=platform.id,
code=tdef["code"],
name=tdef["name"],
price_monthly_cents=tdef["price_monthly_cents"],
price_annual_cents=tdef["price_annual_cents"],
is_public=tdef["is_public"],
display_order=tdef["display_order"],
is_active=True,
)
db.add(tier)
db.flush()
tiers_created += 1
print_success(f"Created tier: {tier.name} ({tier.code})")
return tiers_created
def verify_rbac_schema(db: Session) -> bool:
"""Verify that RBAC schema is in place."""
try:
from sqlalchemy import inspect
inspector = inspect(db.bind)
tables = inspector.get_table_names()
# Check users table has is_email_verified
if "users" in tables:
user_cols = {col["name"] for col in inspector.get_columns("users")}
if "is_email_verified" not in user_cols:
print_error("Missing 'is_email_verified' column in users table")
return False
# Check store_users has RBAC columns
if "store_users" in tables:
vu_cols = {col["name"] for col in inspector.get_columns("store_users")}
required_cols = {
"user_type",
"invitation_token",
"invitation_sent_at",
"invitation_accepted_at",
}
missing = required_cols - vu_cols
if missing:
print_error(f"Missing columns in store_users: {missing}")
return False
# Check roles table exists
if "roles" not in tables:
print_error("Missing 'roles' table")
return False
print_success("RBAC schema verified")
return True
except Exception as e:
print_error(f"Schema verification failed: {e}")
return False
# =============================================================================
# MAIN INITIALIZATION
# =============================================================================
def initialize_production(db: Session, auth_manager: AuthManager):
"""Initialize production database with essential data."""
print_header("PRODUCTION INITIALIZATION")
# Step 1: Verify RBAC schema
print_step(1, "Verifying RBAC schema...")
if not verify_rbac_schema(db):
print_error("RBAC schema not ready. Run migrations first:")
print(" make migrate-up")
sys.exit(1)
# Step 2: Create admin user
print_step(2, "Creating platform admin...")
admin = create_admin_user(db, auth_manager)
# Step 3: Create default platforms
print_step(3, "Creating default platforms...")
platforms = create_default_platforms(db)
# Step 4: Set up default role templates
print_step(4, "Setting up role templates...")
role_templates = create_default_role_templates(db)
# Step 5: Create admin settings
print_step(5, "Creating admin settings...")
create_admin_settings(db)
# Step 6: Seed subscription tiers
print_step(6, "Seeding subscription tiers...")
oms_platform = next((p for p in platforms if p.code == "oms"), None)
if oms_platform:
create_subscription_tiers(db, oms_platform)
else:
print_warning("OMS platform not found, skipping tier seeding")
# Commit all changes
db.commit()
print_success("All changes committed")
def print_summary(db: Session):
"""Print initialization summary."""
print_header("INITIALIZATION SUMMARY")
# Count records
user_count = db.query(User).filter(User.role == "admin").count()
setting_count = db.query(AdminSetting).count()
platform_count = db.query(Platform).count()
tier_count = db.query(SubscriptionTier).filter(SubscriptionTier.is_active.is_(True)).count()
print("\n📊 Database Status:")
print(f" Admin users: {user_count}")
print(f" Platforms: {platform_count}")
print(f" Admin settings: {setting_count}")
print(f" Sub. tiers: {tier_count}")
print("\n" + "" * 70)
print("🔐 ADMIN CREDENTIALS")
print("" * 70)
print(" URL: /admin/login")
print(f" Username: {settings.admin_username}")
print(f" Password: {settings.admin_password}")
print("" * 70)
# Show security warnings if in production
if is_production():
warnings = validate_production_settings()
if warnings:
print("\n⚠️ SECURITY WARNINGS:")
for warning in warnings:
print(f" {warning}")
print("\n Please update your .env file with secure values!")
else:
print("\n⚠️ DEVELOPMENT MODE:")
print(" Default credentials are OK for development")
print(" Change them in production via .env file")
print("\n🚀 NEXT STEPS:")
print(" 1. Login to admin panel")
if is_production():
print(" 2. CHANGE DEFAULT PASSWORD IMMEDIATELY!")
print(" 3. Configure admin settings")
print(" 4. Create first store")
else:
print(" 2. Create demo data: make seed-demo")
print(" 3. Start development: make dev")
print("\n📝 FOR DEMO DATA (Development only):")
print(" make seed-demo")
# =============================================================================
# MAIN ENTRY POINT
# =============================================================================
def main():
"""Main entry point."""
print("\n" + "" + "" * 68 + "")
print("" + " " * 16 + "PRODUCTION INITIALIZATION" + " " * 27 + "")
print("" + "" * 68 + "")
# Show environment info
print_environment_info()
# Production safety check
if is_production():
warnings = validate_production_settings()
if warnings:
print("\n⚠️ PRODUCTION WARNINGS DETECTED:")
for warning in warnings:
print(f" {warning}")
print("\nUpdate your .env file before continuing!")
response = input("\nContinue anyway? (yes/no): ")
if response.lower() != "yes":
print("Initialization cancelled.")
sys.exit(0)
db = SessionLocal()
auth_manager = AuthManager()
try:
initialize_production(db, auth_manager)
print_summary(db)
print_header("✅ INITIALIZATION COMPLETED")
except KeyboardInterrupt:
db.rollback()
print("\n\n⚠️ Initialization interrupted")
sys.exit(1)
except Exception as e:
db.rollback()
print_header("❌ INITIALIZATION FAILED")
print(f"\nError: {e}\n")
import traceback
traceback.print_exc()
sys.exit(1)
finally:
db.close()
if __name__ == "__main__":
main()

595
scripts/seed/install.py Executable file
View File

@@ -0,0 +1,595 @@
#!/usr/bin/env python3
"""
Wizamart Platform Installation Script
This script handles first-time installation of the Wizamart platform:
1. Validates Python version and dependencies
2. Checks environment configuration (.env file)
3. Validates required settings for production
4. Runs database migrations
5. Initializes essential data (admin, settings, CMS, email templates)
6. Provides a configuration status report
Usage:
make platform-install
# or directly:
python scripts/install.py
This script is idempotent - safe to run multiple times.
"""
import os
import subprocess
import sys
from pathlib import Path
# Add project root to path
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
# =============================================================================
# CONSOLE OUTPUT HELPERS
# =============================================================================
class Colors:
"""ANSI color codes for terminal output."""
HEADER = "\033[95m"
BLUE = "\033[94m"
CYAN = "\033[96m"
GREEN = "\033[92m"
WARNING = "\033[93m"
FAIL = "\033[91m"
ENDC = "\033[0m"
BOLD = "\033[1m"
DIM = "\033[2m"
def print_header(text: str):
"""Print a bold header."""
print(f"\n{Colors.BOLD}{Colors.HEADER}{'=' * 70}{Colors.ENDC}")
print(f"{Colors.BOLD}{Colors.HEADER} {text}{Colors.ENDC}")
print(f"{Colors.BOLD}{Colors.HEADER}{'=' * 70}{Colors.ENDC}")
def print_section(text: str):
"""Print a section header."""
print(f"\n{Colors.BOLD}{Colors.CYAN}[*] {text}{Colors.ENDC}")
def print_step(step: int, total: int, text: str):
"""Print a step indicator."""
print(f"\n{Colors.BLUE}[{step}/{total}] {text}{Colors.ENDC}")
def print_success(text: str):
"""Print success message."""
print(f" {Colors.GREEN}{Colors.ENDC} {text}")
def print_warning(text: str):
"""Print warning message."""
print(f" {Colors.WARNING}{Colors.ENDC} {text}")
def print_error(text: str):
"""Print error message."""
print(f" {Colors.FAIL}{Colors.ENDC} {text}")
def print_info(text: str):
"""Print info message."""
print(f" {Colors.DIM}{Colors.ENDC} {text}")
# =============================================================================
# VALIDATION FUNCTIONS
# =============================================================================
def check_python_version() -> bool:
"""Check if Python version is supported."""
major, minor = sys.version_info[:2]
if major < 3 or (major == 3 and minor < 11):
print_error(f"Python 3.11+ required. Found: {major}.{minor}")
return False
print_success(f"Python version: {major}.{minor}")
return True
def check_env_file() -> tuple[bool, dict]:
"""Check if .env file exists and load it."""
env_path = project_root / ".env"
env_example_path = project_root / ".env.example"
if not env_path.exists():
if env_example_path.exists():
print_warning(".env file not found")
print_info("Copy .env.example to .env and configure it:")
print_info(" cp .env.example .env")
return False, {}
else:
print_warning("Neither .env nor .env.example found")
return False, {}
print_success(".env file found")
# Load .env manually to check values
env_vars = {}
with open(env_path) as f:
for line in f:
line = line.strip()
if line and not line.startswith("#") and "=" in line:
key, _, value = line.partition("=")
# Remove quotes if present
value = value.strip().strip("'\"")
env_vars[key.strip()] = value
return True, env_vars
def validate_configuration(env_vars: dict) -> dict:
"""
Validate configuration and return status for each category.
Returns dict with categories and their status:
{
"category": {
"status": "ok" | "warning" | "missing",
"message": "...",
"items": [...]
}
}
"""
results = {}
# -------------------------------------------------------------------------
# Database
# -------------------------------------------------------------------------
db_url = env_vars.get("DATABASE_URL", "")
if db_url and "sqlite" not in db_url.lower():
results["database"] = {
"status": "ok",
"message": "Production database configured",
"items": [f"URL: {db_url[:50]}..."]
}
elif db_url and "sqlite" in db_url.lower():
results["database"] = {
"status": "warning",
"message": "SQLite database (OK for development)",
"items": ["Consider PostgreSQL for production"]
}
else:
results["database"] = {
"status": "ok",
"message": "Using default SQLite database",
"items": ["Configure DATABASE_URL for production"]
}
# -------------------------------------------------------------------------
# Security (JWT)
# -------------------------------------------------------------------------
jwt_key = env_vars.get("JWT_SECRET_KEY", "")
if jwt_key and jwt_key != "change-this-in-production" and len(jwt_key) >= 32:
results["security"] = {
"status": "ok",
"message": "JWT secret configured",
"items": ["Secret key is set and sufficiently long"]
}
elif jwt_key and jwt_key == "change-this-in-production":
results["security"] = {
"status": "warning",
"message": "Using default JWT secret",
"items": [
"CRITICAL: Change JWT_SECRET_KEY for production!",
"Use: openssl rand -hex 32"
]
}
else:
results["security"] = {
"status": "warning",
"message": "JWT secret not explicitly set",
"items": ["Set JWT_SECRET_KEY in .env"]
}
# -------------------------------------------------------------------------
# Admin Credentials
# -------------------------------------------------------------------------
admin_pass = env_vars.get("ADMIN_PASSWORD", "admin123")
admin_email = env_vars.get("ADMIN_EMAIL", "admin@wizamart.com")
if admin_pass != "admin123":
results["admin"] = {
"status": "ok",
"message": "Admin credentials configured",
"items": [f"Email: {admin_email}"]
}
else:
results["admin"] = {
"status": "warning",
"message": "Using default admin password",
"items": [
"Set ADMIN_PASSWORD in .env",
"Change immediately after first login"
]
}
# -------------------------------------------------------------------------
# Stripe Billing
# -------------------------------------------------------------------------
stripe_secret = env_vars.get("STRIPE_SECRET_KEY", "")
stripe_pub = env_vars.get("STRIPE_PUBLISHABLE_KEY", "")
stripe_webhook = env_vars.get("STRIPE_WEBHOOK_SECRET", "")
if stripe_secret and stripe_pub:
if stripe_secret.startswith("sk_live_"):
results["stripe"] = {
"status": "ok",
"message": "Stripe LIVE mode configured",
"items": [
"Live secret key set",
f"Webhook secret: {'configured' if stripe_webhook else 'NOT SET'}"
]
}
elif stripe_secret.startswith("sk_test_"):
results["stripe"] = {
"status": "warning",
"message": "Stripe TEST mode configured",
"items": [
"Using test keys (OK for development)",
"Switch to live keys for production"
]
}
else:
results["stripe"] = {
"status": "warning",
"message": "Stripe keys set but format unclear",
"items": ["Verify STRIPE_SECRET_KEY format"]
}
else:
results["stripe"] = {
"status": "missing",
"message": "Stripe not configured",
"items": [
"Set STRIPE_SECRET_KEY",
"Set STRIPE_PUBLISHABLE_KEY",
"Set STRIPE_WEBHOOK_SECRET",
"Billing features will not work!"
]
}
# -------------------------------------------------------------------------
# Email Configuration
# -------------------------------------------------------------------------
email_provider = env_vars.get("EMAIL_PROVIDER", "smtp")
email_enabled = env_vars.get("EMAIL_ENABLED", "true").lower() == "true"
email_debug = env_vars.get("EMAIL_DEBUG", "false").lower() == "true"
if not email_enabled:
results["email"] = {
"status": "warning",
"message": "Email disabled",
"items": ["EMAIL_ENABLED=false - no emails will be sent"]
}
elif email_debug:
results["email"] = {
"status": "warning",
"message": "Email in debug mode",
"items": ["EMAIL_DEBUG=true - emails logged, not sent"]
}
elif email_provider == "smtp":
smtp_host = env_vars.get("SMTP_HOST", "localhost")
smtp_user = env_vars.get("SMTP_USER", "")
if smtp_host != "localhost" and smtp_user:
results["email"] = {
"status": "ok",
"message": f"SMTP configured ({smtp_host})",
"items": [f"User: {smtp_user}"]
}
else:
results["email"] = {
"status": "warning",
"message": "SMTP using defaults",
"items": [
"Configure SMTP_HOST, SMTP_USER, SMTP_PASSWORD",
"Or use EMAIL_DEBUG=true for development"
]
}
elif email_provider == "sendgrid":
api_key = env_vars.get("SENDGRID_API_KEY", "")
if api_key:
results["email"] = {
"status": "ok",
"message": "SendGrid configured",
"items": ["API key set"]
}
else:
results["email"] = {
"status": "missing",
"message": "SendGrid selected but not configured",
"items": ["Set SENDGRID_API_KEY"]
}
elif email_provider == "mailgun":
api_key = env_vars.get("MAILGUN_API_KEY", "")
domain = env_vars.get("MAILGUN_DOMAIN", "")
if api_key and domain:
results["email"] = {
"status": "ok",
"message": f"Mailgun configured ({domain})",
"items": ["API key and domain set"]
}
else:
results["email"] = {
"status": "missing",
"message": "Mailgun selected but not configured",
"items": ["Set MAILGUN_API_KEY and MAILGUN_DOMAIN"]
}
elif email_provider == "ses":
access_key = env_vars.get("AWS_ACCESS_KEY_ID", "")
secret_key = env_vars.get("AWS_SECRET_ACCESS_KEY", "")
if access_key and secret_key:
results["email"] = {
"status": "ok",
"message": "Amazon SES configured",
"items": [f"Region: {env_vars.get('AWS_REGION', 'eu-west-1')}"]
}
else:
results["email"] = {
"status": "missing",
"message": "SES selected but not configured",
"items": ["Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY"]
}
# -------------------------------------------------------------------------
# Platform Domain
# -------------------------------------------------------------------------
domain = env_vars.get("PLATFORM_DOMAIN", "wizamart.com")
if domain != "wizamart.com":
results["domain"] = {
"status": "ok",
"message": f"Custom domain: {domain}",
"items": []
}
else:
results["domain"] = {
"status": "warning",
"message": "Using default domain",
"items": ["Set PLATFORM_DOMAIN for your deployment"]
}
# -------------------------------------------------------------------------
# Celery / Redis Task Queue
# -------------------------------------------------------------------------
redis_url = env_vars.get("REDIS_URL", "")
use_celery = env_vars.get("USE_CELERY", "false").lower() == "true"
flower_url = env_vars.get("FLOWER_URL", "")
flower_password = env_vars.get("FLOWER_PASSWORD", "")
if use_celery:
if redis_url:
celery_items = [f"Redis: {redis_url.split('@')[-1] if '@' in redis_url else redis_url}"]
if flower_url:
celery_items.append(f"Flower: {flower_url}")
else:
celery_items.append("FLOWER_URL not set (monitoring disabled)")
if flower_password and flower_password != "changeme":
celery_items.append("Flower password configured")
elif flower_password == "changeme":
celery_items.append("WARNING: Change FLOWER_PASSWORD for production!")
results["celery"] = {
"status": "ok",
"message": "Celery enabled with Redis",
"items": celery_items
}
else:
results["celery"] = {
"status": "missing",
"message": "Celery enabled but Redis not configured",
"items": [
"Set REDIS_URL (e.g., redis://localhost:6379/0)",
"Or disable Celery: USE_CELERY=false"
]
}
else:
results["celery"] = {
"status": "warning",
"message": "Celery disabled (using FastAPI BackgroundTasks)",
"items": [
"Set USE_CELERY=true for production",
"Requires Redis: docker-compose up -d redis"
]
}
return results
def print_configuration_report(config_status: dict):
"""Print a formatted configuration status report."""
print_section("Configuration Status")
ok_count = 0
warning_count = 0
missing_count = 0
for category, status in config_status.items():
if status["status"] == "ok":
icon = f"{Colors.GREEN}{Colors.ENDC}"
ok_count += 1
elif status["status"] == "warning":
icon = f"{Colors.WARNING}{Colors.ENDC}"
warning_count += 1
else:
icon = f"{Colors.FAIL}{Colors.ENDC}"
missing_count += 1
print(f"\n {icon} {Colors.BOLD}{category.upper()}{Colors.ENDC}: {status['message']}")
for item in status.get("items", []):
print(f" {Colors.DIM}{item}{Colors.ENDC}")
print(f"\n {Colors.DIM}" * 35 + Colors.ENDC)
print(f" Summary: {Colors.GREEN}{ok_count} OK{Colors.ENDC}, "
f"{Colors.WARNING}{warning_count} warnings{Colors.ENDC}, "
f"{Colors.FAIL}{missing_count} missing{Colors.ENDC}")
return ok_count, warning_count, missing_count
# =============================================================================
# INSTALLATION STEPS
# =============================================================================
def run_migrations() -> bool:
"""Run database migrations."""
try:
result = subprocess.run(
[sys.executable, "-m", "alembic", "upgrade", "head"],
cwd=project_root,
capture_output=True,
text=True
)
if result.returncode == 0:
print_success("Migrations completed successfully")
return True
else:
print_error("Migration failed")
if result.stderr:
print_info(result.stderr[:500])
return False
except Exception as e:
print_error(f"Failed to run migrations: {e}")
return False
def run_init_script(script_name: str, description: str) -> bool:
"""Run an initialization script."""
script_path = project_root / "scripts" / script_name
try:
result = subprocess.run(
[sys.executable, str(script_path)],
cwd=project_root,
capture_output=True,
text=True
)
if result.returncode == 0:
print_success(description)
return True
else:
print_error(f"Failed: {description}")
if result.stderr:
print_info(result.stderr[:300])
return False
except Exception as e:
print_error(f"Error running {script_name}: {e}")
return False
# =============================================================================
# MAIN INSTALLATION FLOW
# =============================================================================
def main():
"""Main installation flow."""
print_header("WIZAMART PLATFORM INSTALLATION")
# -------------------------------------------------------------------------
# Step 1: Pre-flight checks
# -------------------------------------------------------------------------
print_step(1, 4, "Pre-flight checks")
if not check_python_version():
sys.exit(1)
env_exists, env_vars = check_env_file()
if not env_exists:
print()
print_warning("Installation can continue with defaults,")
print_warning("but you should configure .env before going to production.")
print()
response = input("Continue with default configuration? [y/N]: ")
if response.lower() != "y":
print("\nInstallation cancelled. Please configure .env first.")
sys.exit(0)
env_vars = {}
# -------------------------------------------------------------------------
# Step 2: Validate configuration
# -------------------------------------------------------------------------
print_step(2, 4, "Validating configuration")
config_status = validate_configuration(env_vars)
ok_count, warning_count, missing_count = print_configuration_report(config_status)
if missing_count > 0:
print()
print_warning(f"{missing_count} critical configuration(s) missing!")
print_warning("The platform may not function correctly.")
response = input("\nContinue anyway? [y/N]: ")
if response.lower() != "y":
print("\nInstallation cancelled. Please fix configuration first.")
sys.exit(0)
# -------------------------------------------------------------------------
# Step 3: Database setup
# -------------------------------------------------------------------------
print_step(3, 4, "Database setup")
print_info("Running database migrations...")
if not run_migrations():
print_error("Failed to run migrations. Cannot continue.")
sys.exit(1)
# -------------------------------------------------------------------------
# Step 4: Initialize platform data
# -------------------------------------------------------------------------
print_step(4, 4, "Initializing platform data")
init_scripts = [
("init_production.py", "Admin user and platform settings"),
("init_log_settings.py", "Log settings"),
("create_default_content_pages.py", "Default CMS pages"),
("create_platform_pages.py", "Platform pages and landing"),
("seed_email_templates.py", "Email templates"),
]
all_success = True
for script, description in init_scripts:
if not run_init_script(script, description):
all_success = False
# -------------------------------------------------------------------------
# Summary
# -------------------------------------------------------------------------
print_header("INSTALLATION COMPLETE")
if all_success:
print(f"\n {Colors.GREEN}✓ Platform installed successfully!{Colors.ENDC}")
else:
print(f"\n {Colors.WARNING}⚠ Installation completed with some errors{Colors.ENDC}")
print(f"\n {Colors.BOLD}Next Steps:{Colors.ENDC}")
print(" 1. Review any warnings above")
if warning_count > 0 or missing_count > 0:
print(" 2. Update .env with production values")
print(" 3. Run 'make platform-install' again to verify")
else:
print(" 2. Start the application: make dev")
print(f"\n {Colors.BOLD}Admin Login:{Colors.ENDC}")
admin_email = env_vars.get("ADMIN_EMAIL", "admin@wizamart.com")
print(f" URL: /admin/login")
print(f" Email: {admin_email}")
print(f" Password: {'(configured in .env)' if env_vars.get('ADMIN_PASSWORD') else 'admin123'}")
if not env_vars.get("ADMIN_PASSWORD"):
print(f"\n {Colors.WARNING}⚠ CHANGE DEFAULT PASSWORD IMMEDIATELY!{Colors.ENDC}")
print(f"\n {Colors.BOLD}For demo data (development only):{Colors.ENDC}")
print(" make seed-demo")
print()
if __name__ == "__main__":
main()

1267
scripts/seed/seed_demo.py Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff