From e3ed4a32957c13c562e996766fb330be330a7ef8 Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Sat, 15 Nov 2025 20:57:39 +0100 Subject: [PATCH] data seed feature for demo and prod --- .env | 109 +- Makefile | 347 +++-- app/api/v1/vendor/__init__.py | 4 +- app/api/v1/vendor/{teams.py => team.py} | 0 app/core/config.py | 238 ++- .../SEED_DATA/DATABASE_INIT_GUIDE.md | 1005 ++++++++++++ .../DATABASE_QUICK_REFERENCE_GUIDE.md | 247 +++ .../product_migration_plan.md | 1131 ++++++++++++++ .../product_migration_quick_ref.md | 91 ++ make-venv.bat | 3 - models/schema/team.py | 8 +- scripts/create_admin.py | 167 -- scripts/init_production.py | 416 +++++ scripts/seed_database.py | 1349 ----------------- scripts/seed_demo.py | 658 ++++++++ scripts/show-frontend-structure.bat | 66 - scripts/show_structure.py | 528 +++++++ 17 files changed, 4574 insertions(+), 1793 deletions(-) rename app/api/v1/vendor/{teams.py => team.py} (100%) create mode 100644 docs/__REVAMPING/SEED_DATA/DATABASE_INIT_GUIDE.md create mode 100644 docs/__REVAMPING/SEED_DATA/DATABASE_QUICK_REFERENCE_GUIDE.md create mode 100644 docs/__REVAMPING/__PROJECT_ROADMAP/PRODUCT MIGRATION/product_migration_plan.md create mode 100644 docs/__REVAMPING/__PROJECT_ROADMAP/PRODUCT MIGRATION/product_migration_quick_ref.md delete mode 100644 make-venv.bat delete mode 100644 scripts/create_admin.py create mode 100644 scripts/init_production.py delete mode 100644 scripts/seed_database.py create mode 100644 scripts/seed_demo.py delete mode 100644 scripts/show-frontend-structure.bat create mode 100644 scripts/show_structure.py diff --git a/.env b/.env index 01271fbb..f9a97d63 100644 --- a/.env +++ b/.env @@ -1,53 +1,104 @@ -# .env.example +# ============================================================================= +# ENVIRONMENT CONFIGURATION +# ============================================================================= +DEBUG=True -# Project information -PROJECT_NAME=Ecommerce Backend API with Marketplace Support -DESCRIPTION=Advanced product management system with JWT authentication -VERSION=0.0.1 +# ============================================================================= +# PROJECT INFORMATION +# ============================================================================= +PROJECT_NAME=Wizamart - Multi-Vendor Marketplace Platform +DESCRIPTION=Multi-tenants multi-themes ecommerce application +VERSION=2.2.0 -# Database Configuration -# DATABASE_URL=postgresql://username:password@localhost:5432/wizamart_db -# For development, you can use SQLite: +# ============================================================================= +# DATABASE CONFIGURATION +# ============================================================================= +# For development (SQLite) DATABASE_URL=sqlite:///./wizamart.db -# Documentation -# .env.development -DOCUMENTATION_URL=http://localhost:8001 -# .env.production -# DOCUMENTATION_URL=https://yourdomain.com/docs -# .env.staging -# DOCUMENTATION_URL=https://staging-docs.yourdomain.com +# For production (PostgreSQL) +# DATABASE_URL=postgresql://username:password@localhost:5432/wizamart_db -# JWT Configuration +# ============================================================================= +# ADMIN INITIALIZATION +# ============================================================================= +# These are used by init_production.py to create the platform admin +# ⚠️ CHANGE THESE IN PRODUCTION! +ADMIN_EMAIL=admin@wizamart.com +ADMIN_USERNAME=admin +ADMIN_PASSWORD=admin123 +ADMIN_FIRST_NAME=Platform +ADMIN_LAST_NAME=Administrator + +# ============================================================================= +# JWT CONFIGURATION +# ============================================================================= JWT_SECRET_KEY=your-super-secret-jwt-key-change-in-production JWT_EXPIRE_HOURS=24 JWT_EXPIRE_MINUTES=30 -# API Configuration +# ============================================================================= +# API SERVER +# ============================================================================= API_HOST=0.0.0.0 API_PORT=8000 -DEBUG=False -# Rate Limiting +# ============================================================================= +# DOCUMENTATION +# ============================================================================= +# Development +DOCUMENTATION_URL=http://localhost:8001 +# Staging +# DOCUMENTATION_URL=https://staging-docs.wizamart.com +# Production +# DOCUMENTATION_URL=https://docs.wizamart.com + +# ============================================================================= +# RATE LIMITING +# ============================================================================= RATE_LIMIT_ENABLED=True RATE_LIMIT_REQUESTS=100 RATE_LIMIT_WINDOW=3600 -# Logging +# ============================================================================= +# LOGGING +# ============================================================================= LOG_LEVEL=DEBUG -LOG_FILE=log/app.log +LOG_FILE=logs/app.log -# Platform domain configuration -PLATFORM_DOMAIN=platform.com # Your main platform domain +# ============================================================================= +# PLATFORM DOMAIN CONFIGURATION +# ============================================================================= +# Your main platform domain +PLATFORM_DOMAIN=wizamart.com # Custom domain features -ALLOW_CUSTOM_DOMAINS=True # Enable/disable custom domains -REQUIRE_DOMAIN_VERIFICATION=True # Require DNS verification +# Enable/disable custom domains +ALLOW_CUSTOM_DOMAINS=True +# Require DNS verification +REQUIRE_DOMAIN_VERIFICATION=True -# SSL/TLS configuration for custom domains -SSL_PROVIDER=letsencrypt # or "cloudflare", "manual" -AUTO_PROVISION_SSL=False # Set to True if using automated SSL +# SSL/TLS configuration + # "letsencrypt" or "cloudflare", "manual" +SSL_PROVIDER=letsencrypt +# Set to True if using automated SSL +AUTO_PROVISION_SSL=False # DNS verification DNS_VERIFICATION_PREFIX=_wizamart-verify -DNS_VERIFICATION_TTL=3600 \ No newline at end of file +DNS_VERIFICATION_TTL=3600 + +# ============================================================================= +# PLATFORM LIMITS +# ============================================================================= +MAX_VENDORS_PER_USER=5 +MAX_TEAM_MEMBERS_PER_VENDOR=50 +INVITATION_EXPIRY_DAYS=7 + +# ============================================================================= +# DEMO/SEED DATA CONFIGURATION (Development only) +# ============================================================================= +SEED_DEMO_VENDORS=3 +SEED_CUSTOMERS_PER_VENDOR=15 +SEED_PRODUCTS_PER_VENDOR=20 +SEED_ORDERS_PER_VENDOR=10 \ No newline at end of file diff --git a/Makefile b/Makefile index ddb9de71..08faaa9d 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,16 @@ -# Multitenant ecommerce application Makefile (Windows Compatible) +# Wizamart Multi-Tenant E-Commerce Platform Makefile +# Cross-platform compatible (Windows & Linux) + .PHONY: install install-dev install-docs install-all dev test test-coverage lint format check docker-build docker-up docker-down clean help -# Detect if we're in a virtual environment and set Python path accordingly +# Detect OS +ifeq ($(OS),Windows_NT) + DETECTED_OS := Windows +else + DETECTED_OS := $(shell uname -s) +endif + +# Set Python based on OS PYTHON := python PIP := pip @@ -23,9 +32,9 @@ install-docs: install-all: install install-dev install-test install-docs -setup: install-all migrate-up - @echo Development environment setup complete! - @echo Run 'make dev' to start development server +setup: install-all migrate-up init-prod + @echo "✅ Development environment setup complete!" + @echo "Run 'make dev' to start development server" # ============================================================================= # DEVELOPMENT SERVERS @@ -34,78 +43,96 @@ setup: install-all migrate-up dev: $(PYTHON) -m uvicorn main:app --reload --host 0.0.0.0 --port 8000 -dev-with-docs: - @echo Starting API and documentation servers... - @start /B $(PYTHON) -m uvicorn main:app --reload --host 0.0.0.0 --port 8000 - @timeout /t 3 >nul - @$(PYTHON) -m mkdocs serve --dev-addr=0.0.0.0:8001 - -dev-full: dev-with-docs - @echo Development environment ready! - @echo API server: http://localhost:8000 - @echo API docs: http://localhost:8000/docs - @echo Documentation: http://localhost:8001 - # ============================================================================= # DATABASE MIGRATIONS # ============================================================================= migrate-create: - @if [ "$(message)" = "" ]; then \ +ifeq ($(DETECTED_OS),Windows) + @if "$(message)"=="" (echo Error: Please provide a message. Usage: make migrate-create message="your_description") else ($(PYTHON) -m alembic revision --autogenerate -m "$(message)") +else + @if [ -z "$(message)" ]; then \ echo "Error: Please provide a message. Usage: make migrate-create message=\"your_description\""; \ else \ $(PYTHON) -m alembic revision --autogenerate -m "$(message)"; \ fi +endif migrate-create-manual: +ifeq ($(DETECTED_OS),Windows) @if "$(message)"=="" (echo Error: Please provide a message. Usage: make migrate-create-manual message="your_description") else ($(PYTHON) -m alembic revision -m "$(message)") +else + @if [ -z "$(message)" ]; then \ + echo "Error: Please provide a message. Usage: make migrate-create-manual message=\"your_description\""; \ + else \ + $(PYTHON) -m alembic revision -m "$(message)"; \ + fi +endif migrate-up: - @echo Running database migrations... + @echo "Running database migrations..." $(PYTHON) -m alembic upgrade head - @echo Migrations completed successfully + @echo "✅ Migrations completed successfully" migrate-down: - @echo Rolling back last migration... + @echo "Rolling back last migration..." $(PYTHON) -m alembic downgrade -1 - @echo Rollback completed + @echo "✅ Rollback completed" migrate-status: - @echo Current migration status: + @echo "Current migration status:" $(PYTHON) -m alembic current - @echo. - @echo Migration history: + @echo "" + @echo "Migration history:" $(PYTHON) -m alembic history --verbose +# ============================================================================= +# DATABASE INITIALIZATION & SEEDING +# ============================================================================= + +init-prod: + @echo "🔧 Initializing production database..." + $(PYTHON) scripts/init_production.py + @echo "✅ Production initialization completed" + +# Demo data seeding - Cross-platform using Python to set environment +seed-demo: + @echo "🎪 Seeding demo data (normal mode)..." +ifeq ($(DETECTED_OS),Windows) + @set SEED_MODE=normal&& $(PYTHON) scripts/seed_demo.py +else + SEED_MODE=normal $(PYTHON) scripts/seed_demo.py +endif + @echo "✅ Demo seeding completed" + +seed-demo-minimal: + @echo "🎪 Seeding demo data (minimal mode - 1 vendor only)..." +ifeq ($(DETECTED_OS),Windows) + @set SEED_MODE=minimal&& $(PYTHON) scripts/seed_demo.py +else + SEED_MODE=minimal $(PYTHON) scripts/seed_demo.py +endif + @echo "✅ Minimal demo seeding completed" + +seed-demo-reset: + @echo "⚠️ WARNING: This will DELETE ALL existing data!" +ifeq ($(DETECTED_OS),Windows) + @set SEED_MODE=reset&& $(PYTHON) scripts/seed_demo.py +else + SEED_MODE=reset $(PYTHON) scripts/seed_demo.py +endif + +db-setup: migrate-up init-prod seed-demo + @echo "✅ Database setup complete!" + @echo "Run 'make dev' to start development server" + +db-reset: migrate-down migrate-up seed-demo-reset + @echo "✅ Database completely reset!" + backup-db: - @echo Creating database backup... + @echo "Creating database backup..." @$(PYTHON) scripts/backup_database.py -# Add these commands to the DATABASE section after backup-db: - -seed: - @echo Seeding database with comprehensive test data... - $(PYTHON) scripts/seed_database.py - @echo Seeding completed successfully - -seed-minimal: - @echo Seeding database with minimal data (admin + 1 vendor)... - $(PYTHON) scripts/seed_database.py --minimal - @echo Minimal seeding completed - -seed-reset: - @echo WARNING: This will DELETE ALL existing data! - $(PYTHON) scripts/seed_database.py --reset - @echo Database reset and seeded - -# Complete database setup (migrate + seed) -db-setup: migrate-up seed - @echo Database setup complete! - @echo Run 'make dev' to start development server - -db-reset: migrate-down migrate-up seed-reset - @echo Database completely reset! - # ============================================================================= # TESTING # ============================================================================= @@ -142,54 +169,54 @@ test-inventory: # ============================================================================= format: - @echo Running black... + @echo "Running black..." $(PYTHON) -m black . --exclude venv - @echo Running isort... + @echo "Running isort..." $(PYTHON) -m isort . --skip venv lint: - @echo Running linting... + @echo "Running linting..." $(PYTHON) -m ruff check . --exclude venv $(PYTHON) -m mypy . --ignore-missing-imports --exclude venv -# Alternative lint if still using flake8 lint-flake8: - @echo Running linting... + @echo "Running linting..." $(PYTHON) -m flake8 . --max-line-length=120 --extend-ignore=E203,W503,I201,I100 --exclude=venv,__pycache__,.git $(PYTHON) -m mypy . --ignore-missing-imports --exclude venv -# Combined format and lint check: format lint -# Combined test with coverage and linting ci: format lint test-coverage -# Quality assurance workflow qa: format lint test-coverage docs-check - @echo Quality assurance checks completed! + @echo "Quality assurance checks completed!" # ============================================================================= # DOCUMENTATION # ============================================================================= docs-serve: - @echo Starting documentation server... + @echo "Starting documentation server..." $(PYTHON) -m mkdocs serve --dev-addr=0.0.0.0:8001 docs-build: - @echo Building documentation... + @echo "Building documentation..." $(PYTHON) -m mkdocs build --clean --strict docs-deploy: - @echo Deploying documentation... + @echo "Deploying documentation..." $(PYTHON) -m mkdocs gh-deploy --clean docs-clean: +ifeq ($(DETECTED_OS),Windows) @if exist site rmdir /s /q site - @echo Documentation build files cleaned! +else + @rm -rf site +endif + @echo "Documentation build files cleaned!" docs-check: - @echo Checking documentation for issues... + @echo "Checking documentation for issues..." $(PYTHON) -m mkdocs build --strict --verbose # ============================================================================= @@ -222,30 +249,43 @@ deploy-prod: migrate-up # ============================================================================= clean: +ifeq ($(DETECTED_OS),Windows) @if exist htmlcov rmdir /s /q htmlcov @if exist .pytest_cache rmdir /s /q .pytest_cache @if exist .coverage del .coverage @if exist .mypy_cache rmdir /s /q .mypy_cache @for /d /r . %%d in (__pycache__) do @if exist "%%d" rmdir /s /q "%%d" @del /s /q *.pyc 2>nul || echo No .pyc files found +else + @find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true + @find . -type f -name "*.pyc" -delete 2>/dev/null || true + @rm -rf htmlcov .pytest_cache .coverage .mypy_cache 2>/dev/null || true +endif + @echo "Cleaned up build artifacts" verify-setup: - @echo Running setup verification... + @echo "Running setup verification..." @$(PYTHON) scripts/verify_setup.py -# Check Python and virtual environment check-env: - @echo Checking Python environment... - @echo Python version: + @echo "Checking Python environment..." + @echo "Detected OS: $(DETECTED_OS)" + @echo "" + @echo "Python version:" @$(PYTHON) --version - @echo. - @echo Python location: + @echo "" +ifeq ($(DETECTED_OS),Windows) + @echo "Python location:" @where $(PYTHON) - @echo. - @echo Virtual environment active: +else + @echo "Python location:" + @which $(PYTHON) +endif + @echo "" + @echo "Virtual environment active:" @$(PYTHON) -c "import sys; print('YES' if hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix) else 'NO')" - @echo. - @echo Python executable: + @echo "" + @echo "Python executable:" @$(PYTHON) -c "import sys; print(sys.executable)" # ============================================================================= @@ -253,75 +293,92 @@ check-env: # ============================================================================= help: - @echo LetzShop API Development Commands - @echo. - @echo === SETUP === - @echo install - Install production dependencies - @echo install-test - Install test dependencies only - @echo install-dev - Install development dependencies - @echo install-all - Install all dependencies - @echo setup - Complete development setup - @echo. - @echo === DEVELOPMENT === - @echo dev - Start API development server - @echo dev-full - Start API and documentation servers - @echo. - @echo === DATABASE === - @echo migrate-create message="msg" - Create new migration - @echo migrate-up - Apply pending migrations - @echo migrate-down - Rollback last migration - @echo migrate-status - Show migration status - @echo backup-db - Backup database - @echo. - @echo === TESTING === - @echo test - Run all tests - @echo test-coverage - Run tests with coverage - @echo test-fast - Run fast tests only - @echo. - @echo === CODE QUALITY === - @echo format - Format code (black + isort) - @echo lint - Run linting (ruff + mypy) - @echo lint-flake8 - Run linting (flake8 + mypy - alternative) - @echo check - Format + lint - @echo ci - Full CI pipeline - @echo qa - Quality assurance (format, lint, test, docs check) - @echo. - @echo === DOCUMENTATION === - @echo docs-serve - Start documentation server - @echo docs-build - Build documentation - @echo. - @echo === DOCKER === - @echo docker-build - Build Docker containers - @echo docker-up - Start Docker containers - @echo docker-down - Stop Docker containers - @echo docker-restart - Restart Docker containers - @echo. - @echo === DEPLOYMENT === - @echo deploy-staging - Deploy to staging environment - @echo deploy-prod - Deploy to production environment - @echo. - @echo === UTILITIES === - @echo clean - Clean build artifacts - @echo verify-setup - Verify project setup - @echo check-env - Check Python and virtual environment - @echo. - @echo === DAILY WORKFLOW === - @echo make dev # Start development - @echo make migrate-create message="feature" # Create migration - @echo make migrate-up # Apply migration - @echo make test # Run tests + @echo "Wizamart Platform Development Commands" + @echo "" + @echo "=== SETUP ===" + @echo " install - Install production dependencies" + @echo " install-test - Install test dependencies only" + @echo " install-dev - Install development dependencies" + @echo " install-all - Install all dependencies" + @echo " setup - Complete development setup" + @echo "" + @echo "=== DEVELOPMENT ===" + @echo " dev - Start API development server" + @echo "" + @echo "=== DATABASE ===" + @echo " migrate-create message=\"msg\" - Create new migration" + @echo " migrate-up - Apply pending migrations" + @echo " migrate-down - Rollback last migration" + @echo " migrate-status - Show migration status" + @echo " init-prod - Initialize production essentials" + @echo " seed-demo - Seed demo data (3 vendors)" + @echo " seed-demo-minimal - Seed minimal demo (1 vendor)" + @echo " seed-demo-reset - DELETE ALL and reseed" + @echo " db-setup - Full dev setup (migrate + init + seed)" + @echo " backup-db - Backup database" + @echo "" + @echo "=== TESTING ===" + @echo " test - Run all tests" + @echo " test-coverage - Run tests with coverage" + @echo " test-fast - Run fast tests only" + @echo "" + @echo "=== CODE QUALITY ===" + @echo " format - Format code (black + isort)" + @echo " lint - Run linting (ruff + mypy)" + @echo " check - Format + lint" + @echo " ci - Full CI pipeline" + @echo " qa - Quality assurance" + @echo "" + @echo "=== DOCUMENTATION ===" + @echo " docs-serve - Start documentation server" + @echo " docs-build - Build documentation" + @echo "" + @echo "=== DOCKER ===" + @echo " docker-build - Build Docker containers" + @echo " docker-up - Start Docker containers" + @echo " docker-down - Stop Docker containers" + @echo "" + @echo "=== UTILITIES ===" + @echo " clean - Clean build artifacts" + @echo " check-env - Check Python environment and OS" + @echo "" + @echo "=== DAILY WORKFLOW ===" + @echo " make setup # Initial setup" + @echo " make dev # Start development" + @echo " make migrate-create message=\"feature\" # Create migration" + @echo " make migrate-up # Apply migration" + @echo " make test # Run tests" help-db: - @echo === DATABASE COMMANDS === - @echo migrate-create message="description" - Create auto-generated migration - @echo migrate-create-manual message="desc" - Create empty migration template - @echo migrate-up - Apply all pending migrations - @echo migrate-down - Rollback last migration - @echo migrate-status - Show current status and history - @echo backup-db - Create database backup - @echo. - @echo TYPICAL WORKFLOW: - @echo 1. Edit your SQLAlchemy models - @echo 2. make migrate-create message="add_new_feature" - @echo 3. Review the generated migration file - @echo 4. make migrate-up + @echo "=== DATABASE COMMANDS ===" + @echo "" + @echo "MIGRATIONS:" + @echo " migrate-create message=\"description\" - Create auto-generated migration" + @echo " migrate-create-manual message=\"desc\" - Create empty migration template" + @echo " migrate-up - Apply all pending migrations" + @echo " migrate-down - Rollback last migration" + @echo " migrate-status - Show current status and history" + @echo "" + @echo "INITIALIZATION:" + @echo " init-prod - Create admin user + settings (SAFE for production)" + @echo "" + @echo "DEMO DATA (Development only):" + @echo " seed-demo - Create 3 demo vendors with data" + @echo " seed-demo-minimal - Create 1 demo vendor only" + @echo " seed-demo-reset - DELETE ALL data and reseed (DANGEROUS!)" + @echo "" + @echo "WORKFLOWS:" + @echo " db-setup - Complete dev setup (migrate + init + seed)" + @echo " db-reset - Nuclear option: rollback + reset + reseed" + @echo "" + @echo "TYPICAL FIRST-TIME SETUP:" + @echo " 1. make migrate-up # Apply migrations" + @echo " 2. make init-prod # Create admin user" + @echo " 3. make seed-demo # Add demo data" + @echo " 4. make dev # Start developing" + @echo "" + @echo "PRODUCTION SETUP:" + @echo " 1. Set ENV=production or ENVIRONMENT=production" + @echo " 2. make migrate-up" + @echo " 3. make init-prod (with secure credentials in .env)" + @echo " 4. Create vendors via admin panel" \ No newline at end of file diff --git a/app/api/v1/vendor/__init__.py b/app/api/v1/vendor/__init__.py index 37b1f281..a0a3b436 100644 --- a/app/api/v1/vendor/__init__.py +++ b/app/api/v1/vendor/__init__.py @@ -22,7 +22,7 @@ from . import ( products, orders, customers, - teams, + team, inventory, marketplace, payments, @@ -55,7 +55,7 @@ router.include_router(settings.router, tags=["vendor-settings"]) router.include_router(products.router, tags=["vendor-products"]) router.include_router(orders.router, tags=["vendor-orders"]) router.include_router(customers.router, tags=["vendor-customers"]) -router.include_router(teams.router, tags=["vendor-teams"]) +router.include_router(team.router, tags=["vendor-team"]) router.include_router(inventory.router, tags=["vendor-inventory"]) router.include_router(marketplace.router, tags=["vendor-marketplace"]) diff --git a/app/api/v1/vendor/teams.py b/app/api/v1/vendor/team.py similarity index 100% rename from app/api/v1/vendor/teams.py rename to app/api/v1/vendor/team.py diff --git a/app/core/config.py b/app/core/config.py index 570194e6..59a5793b 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -1,25 +1,35 @@ # app/core/config.py -"""Summary description .... +""" +Application configuration using Pydantic Settings. This module provides classes and functions for: -- .... -- .... -- .... +- Configuration management via environment variables +- Database settings +- JWT and authentication configuration +- Platform domain and multi-tenancy settings +- Admin initialization settings + +Note: Environment detection is handled by app.core.environment module. +This module focuses purely on configuration storage and validation. """ from typing import List, Optional - -from pydantic_settings import \ - BaseSettings # This is the correct import for Pydantic v2 +from pydantic_settings import BaseSettings class Settings(BaseSettings): - """Settings class inheriting from BaseSettings that allows values to be overridden by environment variables.""" + """ + Settings class for application configuration. - # Project information - project_name: str = "Ecommerce Backend API with Marketplace Support" - # Documentation - documentation_url: str = "http://localhost:8001" # Development default + Environment detection is delegated to app.core.environment. + This class focuses on configuration values only. + """ + + # ============================================================================= + # PROJECT INFORMATION + # ============================================================================= + project_name: str = "Wizamart - Multi-Vendor Marketplace Platform" + version: str = "2.2.0" # Clean description without HTML description: str = """ @@ -34,49 +44,221 @@ class Settings(BaseSettings): **Documentation:** Visit /documentation for complete guides **API Testing:** Use /docs for interactive API exploration """ - version: str = "2.2.0" - # Database - database_url: str = "sqlite:///./ecommerce.db" + # ============================================================================= + # DATABASE + # ============================================================================= + database_url: str = "sqlite:///./wizamart.db" - # JWT + # ============================================================================= + # ADMIN INITIALIZATION (for init_production.py) + # ============================================================================= + admin_email: str = "admin@wizamart.com" + admin_username: str = "admin" + admin_password: str = "admin123" # CHANGE IN PRODUCTION! + admin_first_name: str = "Platform" + admin_last_name: str = "Administrator" + + # ============================================================================= + # JWT AUTHENTICATION + # ============================================================================= jwt_secret_key: str = "change-this-in-production" jwt_expire_hours: int = 24 jwt_expire_minutes: int = 30 - # Middleware - allowed_hosts: List[str] = ["*"] # Configure for production - - # API + # ============================================================================= + # API SERVER + # ============================================================================= api_host: str = "0.0.0.0" api_port: int = 8000 - debug: bool = False + debug: bool = True + + # ============================================================================= + # DOCUMENTATION + # ============================================================================= + documentation_url: str = "http://localhost:8001" + + # ============================================================================= + # MIDDLEWARE & SECURITY + # ============================================================================= + allowed_hosts: List[str] = ["*"] # Configure for production # Rate Limiting rate_limit_enabled: bool = True rate_limit_requests: int = 100 rate_limit_window: int = 3600 - # Logging + # ============================================================================= + # LOGGING + # ============================================================================= log_level: str = "INFO" log_file: Optional[str] = None - # Platform domain configuration - platform_domain: str = "platform.com" # Your main platform domain + # ============================================================================= + # PLATFORM DOMAIN CONFIGURATION + # ============================================================================= + platform_domain: str = "wizamart.com" # Custom domain features - allow_custom_domains: bool = True # Enable/disable custom domains - require_domain_verification: bool = True # Require DNS verification + allow_custom_domains: bool = True + require_domain_verification: bool = True # SSL/TLS configuration for custom domains ssl_provider: str = "letsencrypt" # or "cloudflare", "manual" - auto_provision_ssl: bool = False # Set to True if using automated SSL + auto_provision_ssl: bool = False # DNS verification dns_verification_prefix: str = "_wizamart-verify" dns_verification_ttl: int = 3600 - model_config = {"env_file": ".env"} # Updated syntax for Pydantic v2 + # ============================================================================= + # PLATFORM LIMITS + # ============================================================================= + max_vendors_per_user: int = 5 + max_team_members_per_vendor: int = 50 + invitation_expiry_days: int = 7 + + # ============================================================================= + # DEMO/SEED DATA CONFIGURATION + # ============================================================================= + # Controls for demo data seeding + seed_demo_vendors: int = 3 # Number of demo vendors to create + seed_customers_per_vendor: int = 15 # Customers per vendor + seed_products_per_vendor: int = 20 # Products per vendor + seed_orders_per_vendor: int = 10 # Orders per vendor + + model_config = {"env_file": ".env"} +# Singleton settings instance settings = Settings() + +# ============================================================================= +# ENVIRONMENT UTILITIES - Module-level functions +# ============================================================================= +# Import environment detection utilities +from app.core.environment import ( + get_environment, + is_development, + is_production, + is_staging, + should_use_secure_cookies +) + + +def get_current_environment() -> str: + """ + Get current environment. + + Convenience function that delegates to app.core.environment. + Use this when you need just the environment string. + """ + return get_environment() + + +def is_production_environment() -> bool: + """ + Check if running in production. + + Convenience function that delegates to app.core.environment. + Use this for production-specific logic. + """ + return is_production() + + +def is_development_environment() -> bool: + """ + Check if running in development. + + Convenience function that delegates to app.core.environment. + Use this for development-specific logic. + """ + return is_development() + + +def is_staging_environment() -> bool: + """ + Check if running in staging. + + Convenience function that delegates to app.core.environment. + Use this for staging-specific logic. + """ + return is_staging() + + +# ============================================================================= +# VALIDATION FUNCTIONS +# ============================================================================= + +def validate_production_settings() -> List[str]: + """ + Validate settings for production environment. + + Returns: + List of warning messages if configuration is insecure + """ + warnings = [] + + if is_production(): + # Check for default/insecure values + if settings.admin_password == "admin123": + warnings.append("⚠️ Using default admin password in production!") + + if settings.jwt_secret_key == "change-this-in-production": + warnings.append("⚠️ Using default JWT secret key in production!") + + if settings.debug: + warnings.append("⚠️ Debug mode enabled in production!") + + if "*" in settings.allowed_hosts: + warnings.append("⚠️ ALLOWED_HOSTS is set to wildcard (*) in production!") + + return warnings + + +def print_environment_info(): + """Print current environment configuration.""" + print("\n" + "=" * 70) + print(f" ENVIRONMENT: {get_environment().upper()}") + print("=" * 70) + print(f" Database: {settings.database_url}") + print(f" Debug mode: {settings.debug}") + print(f" API port: {settings.api_port}") + print(f" Platform: {settings.platform_domain}") + print(f" Secure cookies: {should_use_secure_cookies()}") + print("=" * 70 + "\n") + + # Show warnings if in production + if is_production(): + warnings = validate_production_settings() + if warnings: + print("\n⚠️ PRODUCTION WARNINGS:") + for warning in warnings: + print(f" {warning}") + print() + + +# ============================================================================= +# PUBLIC API +# ============================================================================= +__all__ = [ + # Settings singleton + 'settings', + + # Environment detection (re-exported from app.core.environment) + 'get_environment', + 'is_development', + 'is_production', + 'is_staging', + 'should_use_secure_cookies', + + # Convenience functions + 'get_current_environment', + 'is_production_environment', + 'is_development_environment', + 'is_staging_environment', + + # Validation + 'validate_production_settings', + 'print_environment_info', +] diff --git a/docs/__REVAMPING/SEED_DATA/DATABASE_INIT_GUIDE.md b/docs/__REVAMPING/SEED_DATA/DATABASE_INIT_GUIDE.md new file mode 100644 index 00000000..1e27e699 --- /dev/null +++ b/docs/__REVAMPING/SEED_DATA/DATABASE_INIT_GUIDE.md @@ -0,0 +1,1005 @@ +# Database Initialization Guide + +**Wizamart Platform - Database Management Documentation** + +This guide covers the database initialization, seeding, and management workflows for the Wizamart multi-tenant e-commerce platform. + +--- + +## Table of Contents + +1. [Overview](#overview) +2. [Architecture](#architecture) +3. [Configuration](#configuration) +4. [Workflows](#workflows) +5. [Command Reference](#command-reference) +6. [Extending the System](#extending-the-system) +7. [Best Practices](#best-practices) +8. [Troubleshooting](#troubleshooting) + +--- + +## Overview + +### Database Initialization Philosophy + +The Wizamart platform uses a **two-tier initialization system**: + +1. **Production Initialization** (`init_production.py`) + - Creates essential platform infrastructure + - Safe to run in all environments (dev/staging/prod) + - Idempotent (safe to run multiple times) + - No test/demo data + +2. **Demo Data Seeding** (`seed_demo.py`) + - Creates realistic test data for development + - **Development only** - blocked in production + - Configurable via settings + - Multiple seeding modes + +### Key Principles + +- **Single Source of Truth**: All configuration flows through `app.core.config.settings` +- **Environment Aware**: Automatic detection of dev/staging/production environments +- **Type Safe**: Pydantic validates all configuration +- **Makefile First**: All operations via clean `make` commands +- **Production Safe**: Built-in validation and blocking of unsafe operations + +--- + +## Architecture + +### System Components + +``` +┌─────────────────────────────────────────────────────────────┐ +│ .env File │ +│ (Environment-specific configuration) │ +└─────────────────┬───────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ app/core/config.py (Settings) │ +│ - Environment detection (is_production, is_development) │ +│ - Admin initialization settings │ +│ - Demo data configuration │ +│ - Platform limits and constraints │ +└────────────┬─────────────────────┬──────────────────────────┘ + │ │ + ▼ ▼ +┌─────────────────────┐ ┌─────────────────────┐ +│ init_production.py │ │ seed_demo.py │ +│ │ │ │ +│ Creates: │ │ Creates: │ +│ • Admin user │ │ • Demo vendors │ +│ • Admin settings │ │ • Test customers │ +│ • Role templates │ │ • Sample products │ +│ • RBAC schema │ │ • Demo orders │ +│ │ │ • Themes/domains │ +│ │ │ │ +│ Safe for prod: ✅ │ │ Safe for prod: ❌ │ +└─────────────────────┘ └─────────────────────┘ + │ │ + └──────────┬──────────┘ + ▼ + ┌──────────────────┐ + │ Database │ + └──────────────────┘ +``` + +### Data Flow + +1. **Configuration** → `.env` file defines environment and settings +2. **Settings Loading** → `app/core/config.py` loads and validates configuration +3. **Script Execution** → Scripts access configuration via `settings` singleton +4. **Database Operations** → Scripts create/update database records +5. **Validation** → Environment checks and warnings ensure safe operations + +--- + +## Configuration + +### Environment Variables (.env) + +All database initialization is controlled through environment variables in `.env`: + +```bash +# ============================================================================= +# ENVIRONMENT CONFIGURATION +# ============================================================================= +ENVIRONMENT=development # development | staging | production + +# ============================================================================= +# ADMIN INITIALIZATION +# ============================================================================= +ADMIN_EMAIL=admin@wizamart.com +ADMIN_USERNAME=admin +ADMIN_PASSWORD=admin123 # ⚠️ CHANGE IN PRODUCTION! +ADMIN_FIRST_NAME=Platform +ADMIN_LAST_NAME=Administrator + +# ============================================================================= +# DEMO DATA CONFIGURATION (Development only) +# ============================================================================= +SEED_DEMO_VENDORS=3 # Number of demo vendors +SEED_CUSTOMERS_PER_VENDOR=15 # Customers per vendor +SEED_PRODUCTS_PER_VENDOR=20 # Products per vendor +SEED_ORDERS_PER_VENDOR=10 # Orders per vendor + +# ============================================================================= +# PLATFORM LIMITS +# ============================================================================= +MAX_VENDORS_PER_USER=5 +MAX_TEAM_MEMBERS_PER_VENDOR=50 +INVITATION_EXPIRY_DAYS=7 +``` + +### Settings Framework + +Access configuration in code through the settings singleton: + +```python +from app.core.config import settings + +# Environment detection +if settings.is_production: + # Production-specific code + pass +elif settings.is_development: + # Development-specific code + pass + +# Access configuration +admin_email = settings.admin_email +demo_vendor_count = settings.seed_demo_vendors +max_vendors = settings.max_vendors_per_user + +# Environment info +from app.core.config import print_environment_info +print_environment_info() +``` + +### Production Validation + +The system includes automatic validation: + +```python +from app.core.config import validate_production_settings + +# Check for insecure production settings +warnings = validate_production_settings() +if warnings: + for warning in warnings: + print(warning) +``` + +--- + +## Workflows + +### First-Time Production Setup + +**Prerequisites**: Database migrations applied + +```bash +# 1. Configure environment +cat > .env << EOF +ENVIRONMENT=production +DATABASE_URL=postgresql://user:pass@localhost/wizamart +ADMIN_EMAIL=admin@yourcompany.com +ADMIN_USERNAME=admin +ADMIN_PASSWORD=SecurePassword123! +JWT_SECRET_KEY=your-secret-key-here +EOF + +# 2. Run migrations +make migrate-up + +# 3. Initialize production essentials +make init-prod + +# 4. Verify +python -c "from app.core.config import validate_production_settings; \ + print('✅ OK' if not validate_production_settings() else 'Fix warnings')" +``` + +**What happens**: +- Platform admin user created +- Essential admin settings configured +- Role templates initialized +- RBAC schema verified + +**Security**: Always change default credentials in production! + +--- + +### First-Time Development Setup + +**Prerequisites**: Virtual environment activated + +```bash +# Complete setup (migrations + init + demo data) +make db-setup +``` + +This single command runs: +1. `make migrate-up` - Apply database migrations +2. `make init-prod` - Create admin user and settings +3. `make seed-demo` - Create demo vendors and test data + +**Alternative: Step-by-step** +```bash +# 1. Apply migrations +make migrate-up + +# 2. Create admin user +make init-prod + +# 3. Create demo data +make seed-demo # 3 vendors +# OR +make seed-demo-minimal # 1 vendor only +``` + +--- + +### Daily Development Workflow + +#### Starting Development +```bash +# Start the development server +make dev + +# API available at: http://localhost:8000 +# Admin panel: http://localhost:8000/admin/login +# API docs: http://localhost:8000/docs +``` + +#### Refreshing Demo Data +```bash +# Add more demo data (idempotent) +make seed-demo + +# Start fresh with new demo data +make seed-demo-reset +``` + +#### Database Changes +```bash +# 1. Modify your SQLAlchemy models +# 2. Create migration +make migrate-create message="add_new_feature" + +# 3. Review the generated migration in alembic/versions/ + +# 4. Apply migration +make migrate-up + +# 5. Update seed scripts if needed +``` + +--- + +### Production Deployment Workflow + +```bash +# 1. Ensure production .env is configured +ENVIRONMENT=production +ADMIN_PASSWORD=SecurePassword123! # Not default! +JWT_SECRET_KEY=your-secret-key # Not default! + +# 2. Apply migrations +make migrate-up + +# 3. Initialize (if first deployment) +make init-prod + +# 4. Create vendors manually via admin panel +# DO NOT run seed-demo in production! +``` + +**Note**: `seed_demo.py` will refuse to run if `ENVIRONMENT=production` + +--- + +## Command Reference + +### Database Migrations + +```bash +# Create a new migration +make migrate-create message="description" + +# Create empty migration template +make migrate-create-manual message="description" + +# Apply pending migrations +make migrate-up + +# Rollback last migration +make migrate-down + +# Check migration status +make migrate-status +``` + +### Initialization & Seeding + +```bash +# Production initialization (SAFE for all environments) +make init-prod + +# Demo data seeding (DEVELOPMENT ONLY) +make seed-demo # Create 3 vendors with full demo data +make seed-demo-minimal # Create 1 vendor with minimal data +make seed-demo-reset # DELETE ALL DATA and reseed (DANGEROUS!) + +# Complete workflows +make db-setup # Full setup: migrate + init + seed +make db-reset # Nuclear option: rollback + migrate + reset +``` + +### Database Utilities + +```bash +# Backup database +make backup-db + +# Verify setup +make verify-setup + +# Check migration history +make migrate-status +``` + +### Help Commands + +```bash +# General help +make help + +# Database-specific help +make help-db +``` + +--- + +## Extending the System + +### Adding New Admin Settings + +Edit `scripts/init_production.py` to add new platform settings: + +```python +def create_admin_settings(db: Session) -> int: + """Create essential admin settings.""" + + default_settings = [ + # ... existing settings ... + + # Add your new setting + { + "key": "your_new_setting", + "value": str(settings.your_new_setting), # From config + "value_type": "string", # string | integer | boolean + "description": "Description of your setting", + "is_public": False, # True if visible to vendors + }, + ] + # ... rest of function +``` + +### Adding New Demo Data + +Edit `scripts/seed_demo.py` to extend demo data creation: + +```python +def seed_demo_data(db: Session, auth_manager: AuthManager): + """Seed demo data for development.""" + + # ... existing steps ... + + # Add your new demo data + print_step(7, "Creating your demo data...") + create_your_demo_data(db, vendors) + + # ... commit +``` + +Create a new function for your data: + +```python +def create_your_demo_data(db: Session, vendors: List[Vendor]) -> List[YourModel]: + """Create demo data for your feature.""" + + items = [] + for vendor in vendors: + # Create demo items for this vendor + item = YourModel( + vendor_id=vendor.id, + # ... your fields + ) + db.add(item) + items.append(item) + + db.flush() + print_success(f"Created {len(items)} demo items") + return items +``` + +### Adding Configuration Options + +#### 1. Add to Settings Class + +Edit `app/core/config.py`: + +```python +class Settings(BaseSettings): + # ... existing settings ... + + # Your new settings + your_new_setting: str = "default_value" + your_numeric_setting: int = 42 + your_boolean_setting: bool = True + + # Optional with validation + your_list_setting: List[str] = ["item1", "item2"] +``` + +#### 2. Add to .env File + +```bash +# Your new configuration +YOUR_NEW_SETTING=custom_value +YOUR_NUMERIC_SETTING=100 +YOUR_BOOLEAN_SETTING=False +``` + +#### 3. Use in Code + +```python +from app.core.config import settings + +# Access your settings +value = settings.your_new_setting +if settings.your_boolean_setting: + # Do something +``` + +### Adding New Demo Vendor Configurations + +Edit the `DEMO_VENDORS` list in `scripts/seed_demo.py`: + +```python +DEMO_VENDORS = [ + # ... existing vendors ... + + # Your new demo vendor + { + "vendor_code": "YOURSHOP", + "name": "Your Shop Name", + "subdomain": "yourshop", + "description": "Your shop description", + "theme_preset": "modern", # or "classic", "vibrant" + "custom_domain": "yourshop.example.com", # or None + }, +] +``` + +Also add a corresponding user in `DEMO_VENDOR_USERS`. + +### Creating Custom Seeding Modes + +Add new mode detection in `seed_demo.py`: + +```python +# Mode detection (from Makefile) +SEED_MODE = os.getenv('SEED_MODE', 'normal') # normal, minimal, reset, custom + +# In seed_demo_data function +if SEED_MODE == 'custom': + # Your custom seeding logic + create_custom_demo_data(db, auth_manager) +``` + +Add corresponding Makefile command: + +```makefile +seed-demo-custom: + @echo 🎪 Seeding custom demo data... + @set SEED_MODE=custom&& $(PYTHON) scripts/seed_demo.py + @echo ✅ Custom demo seeding completed +``` + +--- + +## Best Practices + +### Configuration Management + +#### ✅ DO: +- Use `settings` for all configuration access +- Define defaults in `Settings` class +- Override via `.env` for environment-specific values +- Use type hints in Settings class +- Document each setting's purpose + +#### ❌ DON'T: +- Use `os.getenv()` directly in scripts +- Hard-code configuration values +- Store secrets in code +- Ignore type validation + +### Environment Detection + +#### ✅ DO: +```python +from app.core.config import settings + +if settings.is_production: + # Production logic +elif settings.is_development: + # Development logic +``` + +#### ❌ DON'T: +```python +# Don't check environment manually +import os +if os.getenv("ENVIRONMENT") == "production": + # This bypasses settings validation +``` + +### Demo Data Creation + +#### ✅ DO: +- Make demo data realistic and useful for testing +- Use idempotent checks (check existence before creating) +- Make demo data configurable via settings +- Include variety in demo data (different states, types) +- Document demo credentials clearly + +#### ❌ DON'T: +- Hard-code demo data counts +- Create unrealistic test data +- Use production-like credentials in demo data +- Skip existence checks (causes duplicates) + +### Script Development + +#### ✅ DO: +- Use helper functions (`print_success`, `print_error`) +- Include comprehensive error handling +- Make operations idempotent +- Validate prerequisites (migrations, admin user) +- Provide clear, actionable error messages + +#### ❌ DON'T: +- Assume database state +- Skip transaction management (db.commit()) +- Ignore exceptions +- Mix production and demo logic in same function + +### Makefile Commands + +#### ✅ DO: +- Use descriptive command names +- Group related commands +- Provide `help` targets +- Use environment variables for modes +- Echo what's happening + +#### ❌ DON'T: +- Pass complex arguments to scripts +- Create ambiguous command names +- Skip error checking +- Hide what commands do + +--- + +## Security Considerations + +### Production Initialization + +**Critical**: Always change default credentials before production deployment! + +```bash +# INSECURE - Default values +ADMIN_PASSWORD=admin123 +JWT_SECRET_KEY=change-this-in-production + +# SECURE - Custom values +ADMIN_PASSWORD=YourSecurePassword123! +JWT_SECRET_KEY=randomly-generated-secret-key-here +``` + +### Production Validation + +The system automatically validates production settings: + +```bash +# Warnings you might see: +⚠️ Using default admin password in production! +⚠️ Using default JWT secret key in production! +⚠️ Debug mode enabled in production! +⚠️ ALLOWED_HOSTS is set to wildcard (*) in production! +``` + +**Action Required**: Update `.env` with secure values + +### Demo Data Protection + +Demo seeding is automatically blocked in production: + +```python +# In seed_demo.py +def check_environment(): + if settings.is_production: + print_error("Cannot run demo seeding in production!") + sys.exit(1) +``` + +This prevents accidental exposure of fake data in production. + +### Credential Management + +#### Production +- **Admin Password**: Strong, unique password +- **JWT Secret**: Randomly generated, never committed +- **Database Password**: Secure, rotated regularly +- **API Keys**: Use environment variables + +#### Development +- **Admin Password**: `admin123` (documented, known to team) +- **Demo Users**: `password123` (clearly insecure) +- **Documentation**: All credentials documented in this guide + +--- + +## Troubleshooting + +### Common Issues + +#### Issue: "Table doesn't exist" Error + +**Cause**: Migrations not applied + +**Solution**: +```bash +# Apply migrations +make migrate-up + +# Verify +make migrate-status +``` + +#### Issue: "Admin user already exists" + +**Cause**: This is normal! Scripts are idempotent. + +**Solution**: No action needed. This message confirms the admin user exists. + +``` +⚠ Admin user already exists: admin@wizamart.com +✓ All changes committed +``` + +#### Issue: "Cannot run demo seeding in production" + +**Cause**: Correct behavior - safety feature working + +**Solution**: +- If you're in development: Set `ENVIRONMENT=development` in `.env` +- If you're in production: Don't seed demo data! Create vendors via admin panel + +#### Issue: "Settings not found" + +**Cause**: Configuration not properly imported + +**Solution**: +```bash +# Verify settings work +python -c "from app.core.config import settings; print(settings.environment)" + +# Check .env file exists +ls -la .env + +# Verify Pydantic is installed +pip list | grep pydantic +``` + +#### Issue: Duplicate Demo Data + +**Cause**: Running seed scripts multiple times without checking existence + +**Solution**: +- Use `make seed-demo-reset` for fresh data +- Check scripts for proper idempotent checks +- Verify database state before seeding + +#### Issue: Migration Conflicts + +**Cause**: Multiple developers creating migrations simultaneously + +**Solution**: +```bash +# Check migration status +make migrate-status + +# Resolve conflicts in alembic/versions/ +# Merge migrations or create a merge migration + +# Apply resolved migrations +make migrate-up +``` + +### Debugging Commands + +```bash +# Check environment configuration +python -c "from app.core.config import print_environment_info; print_environment_info()" + +# Validate production settings +python -c "from app.core.config import validate_production_settings; \ + warnings = validate_production_settings(); \ + print('✅ OK' if not warnings else warnings)" + +# Check database state +python -c " +from app.core.database import SessionLocal +from models.database.vendor import Vendor +from models.database.user import User +db = SessionLocal() +print(f'Users: {db.query(User).count()}') +print(f'Vendors: {db.query(Vendor).count()}') +db.close() +" + +# Test settings import +python -c "from app.core.config import settings; print(f'Env: {settings.environment}'); print(f'Admin: {settings.admin_email}')" +``` + +### Getting Help + +1. **Check this guide** - Most common scenarios are documented +2. **Use help commands** - `make help-db` for command reference +3. **Check logs** - Review output from failed commands +4. **Verify configuration** - Ensure `.env` is properly configured +5. **Check migrations** - Ensure all migrations are applied + +--- + +## Reference + +### Default Credentials + +#### Admin (After `make init-prod`) +``` +URL: http://localhost:8000/admin/login +Username: admin +Password: admin123 (⚠️ CHANGE IN PRODUCTION!) +Email: admin@wizamart.com +``` + +#### Demo Vendors (After `make seed-demo`) +``` +Vendor 1: vendor1@example.com / password123 +Vendor 2: vendor2@example.com / password123 +Vendor 3: vendor3@example.com / password123 +``` + +**⚠️ All demo passwords are intentionally insecure for development use!** + +### File Locations + +``` +wizamart/ +├── .env # Environment configuration +├── Makefile # Command definitions +├── app/ +│ └── core/ +│ └── config.py # Settings framework +├── scripts/ +│ ├── init_production.py # Production initialization +│ └── seed_demo.py # Demo data seeding +└── alembic/ + └── versions/ # Database migrations +``` + +### Key Commands Summary + +```bash +# Setup +make setup # Complete initial setup +make db-setup # Database setup only + +# Migrations +make migrate-up # Apply migrations +make migrate-create message="msg" # Create migration + +# Initialization +make init-prod # Production init (safe everywhere) + +# Demo Data (Development only) +make seed-demo # Full demo data +make seed-demo-minimal # Minimal demo +make seed-demo-reset # Reset and reseed + +# Development +make dev # Start server +make test # Run tests + +# Help +make help # All commands +make help-db # Database commands +``` + +--- + +## Appendices + +### A. Environment Variable Reference + +Complete list of database-related environment variables: + +| Variable | Type | Default | Description | +|----------|------|---------|-------------| +| `ENVIRONMENT` | string | `development` | Environment mode (development/staging/production) | +| `DATABASE_URL` | string | `sqlite:///./wizamart.db` | Database connection string | +| `ADMIN_EMAIL` | string | `admin@wizamart.com` | Platform admin email | +| `ADMIN_USERNAME` | string | `admin` | Platform admin username | +| `ADMIN_PASSWORD` | string | `admin123` | Platform admin password | +| `ADMIN_FIRST_NAME` | string | `Platform` | Admin first name | +| `ADMIN_LAST_NAME` | string | `Administrator` | Admin last name | +| `SEED_DEMO_VENDORS` | integer | `3` | Number of demo vendors to create | +| `SEED_CUSTOMERS_PER_VENDOR` | integer | `15` | Demo customers per vendor | +| `SEED_PRODUCTS_PER_VENDOR` | integer | `20` | Demo products per vendor | +| `SEED_ORDERS_PER_VENDOR` | integer | `10` | Demo orders per vendor | +| `MAX_VENDORS_PER_USER` | integer | `5` | Maximum vendors per user | +| `MAX_TEAM_MEMBERS_PER_VENDOR` | integer | `50` | Maximum team members per vendor | +| `INVITATION_EXPIRY_DAYS` | integer | `7` | Team invitation expiry days | + +### B. Database Tables Created + +#### Production Initialization Tables +- `users` - Platform users (admin, vendors, team members) +- `admin_settings` - Platform configuration settings +- `roles` - RBAC role definitions + +#### Demo Data Tables +- `vendors` - Demo vendor accounts +- `vendor_users` - Vendor-user relationships +- `vendor_themes` - Vendor theme customizations +- `vendor_domains` - Custom domain configurations +- `customers` - Demo customer accounts +- `customer_addresses` - Customer address information +- `products` - Demo product catalog +- `marketplace_products` - Marketplace integrations +- `orders` - Demo order records +- `order_items` - Order line items + +### C. Settings Properties Reference + +Access these via `from app.core.config import settings`: + +```python +# Environment Detection +settings.environment # "development" | "staging" | "production" +settings.is_production # bool +settings.is_development # bool +settings.is_staging # bool + +# Admin Configuration +settings.admin_email # str +settings.admin_username # str +settings.admin_password # str +settings.admin_first_name # str +settings.admin_last_name # str + +# Demo Data Configuration +settings.seed_demo_vendors # int +settings.seed_customers_per_vendor # int +settings.seed_products_per_vendor # int +settings.seed_orders_per_vendor # int + +# Platform Limits +settings.max_vendors_per_user # int +settings.max_team_members_per_vendor # int +settings.invitation_expiry_days # int + +# Database +settings.database_url # str + +# Platform +settings.platform_domain # str +settings.project_name # str +``` + +--- + +## Contributing + +### Adding Features to This System + +When extending the database initialization system: + +1. **Update Settings** (`app/core/config.py`) + - Add configuration variables + - Include type hints + - Provide sensible defaults + - Document the setting + +2. **Update Environment** (`.env`) + - Add environment variables + - Document their purpose + - Provide examples + +3. **Update Scripts** + - Use `settings` for configuration + - Include idempotent checks + - Add helpful output messages + - Handle errors gracefully + +4. **Update Makefile** + - Add commands if needed + - Update help text + - Follow naming conventions + +5. **Update Documentation** + - Document new features + - Update command reference + - Add examples + - Include troubleshooting + +### Code Review Checklist + +When reviewing database initialization changes: + +- [ ] Uses `settings` instead of direct environment access +- [ ] Includes idempotent checks +- [ ] Has appropriate environment detection +- [ ] Includes error handling +- [ ] Updates documentation +- [ ] Adds/updates tests +- [ ] Follows existing patterns +- [ ] Includes helpful output messages + +--- + +## Changelog + +### Current Version (v2.0) + +**Database Initialization System** +- Settings-based configuration +- Environment-aware operations +- Makefile-driven commands +- Production safety features +- Comprehensive documentation + +**Key Features**: +- Idempotent initialization scripts +- Automatic production validation +- Configurable demo data creation +- Clean command interface +- Type-safe configuration + +--- + +## Support + +For questions or issues: + +1. **Check this documentation** - Most scenarios are covered +2. **Review command help** - `make help-db` +3. **Check settings** - Verify `.env` configuration +4. **Review logs** - Check script output for errors +5. **Contact the team** - Reach out if stuck + +**Remember**: This system is designed to be safe, clear, and maintainable. When in doubt, refer to this guide! + +--- + +**Last Updated**: 2025 +**Maintained By**: Wizamart Platform Team +**Version**: 2.0 \ No newline at end of file diff --git a/docs/__REVAMPING/SEED_DATA/DATABASE_QUICK_REFERENCE_GUIDE.md b/docs/__REVAMPING/SEED_DATA/DATABASE_QUICK_REFERENCE_GUIDE.md new file mode 100644 index 00000000..178bb405 --- /dev/null +++ b/docs/__REVAMPING/SEED_DATA/DATABASE_QUICK_REFERENCE_GUIDE.md @@ -0,0 +1,247 @@ +# Database Commands - Quick Reference + +## 🚀 Common Workflows + +### First-Time Development Setup +```bash +make migrate-up # Apply migrations +make init-prod # Create admin user +make seed-demo # Add demo data +make dev # Start developing +``` + +### First-Time Production Setup +```bash +# 1. Configure .env +ENVIRONMENT=production +ADMIN_EMAIL=admin@yourcompany.com +ADMIN_PASSWORD=SecurePassword123! + +# 2. Initialize +make migrate-up +make init-prod + +# 3. Create vendors via admin panel +``` + +### Daily Development +```bash +make dev # Start server +make seed-demo-reset # Fresh demo data +make test # Run tests +``` + +--- + +## 📋 Command Reference + +### Database Migrations +```bash +make migrate-create message="add_feature" # Create migration +make migrate-up # Apply migrations +make migrate-down # Rollback last +make migrate-status # Check status +``` + +### Initialization +```bash +make init-prod # Create admin + settings (SAFE for production) +``` + +### Demo Data (Development Only) +```bash +make seed-demo # 3 vendors + data +make seed-demo-minimal # 1 vendor only +make seed-demo-reset # DELETE ALL + reseed (DANGEROUS!) +``` + +### Complete Workflows +```bash +make db-setup # migrate + init + seed +make db-reset # rollback + migrate + reset +``` + +--- + +## ⚙️ Configuration (.env) + +### Required Settings +```bash +ENVIRONMENT=development # development/staging/production +DATABASE_URL=sqlite:///./wizamart.db + +# Admin credentials (CHANGE IN PRODUCTION!) +ADMIN_EMAIL=admin@wizamart.com +ADMIN_USERNAME=admin +ADMIN_PASSWORD=admin123 +``` + +### Demo Data Controls +```bash +SEED_DEMO_VENDORS=3 # How many vendors +SEED_CUSTOMERS_PER_VENDOR=15 # Customers per vendor +SEED_PRODUCTS_PER_VENDOR=20 # Products per vendor +``` + +### Using Settings in Code +```python +from app.core.config import settings + +# Environment checks +if settings.is_production: + # Production code + +# Access configuration +email = settings.admin_email +db_url = settings.database_url +``` + +--- + +## 🔐 Default Credentials + +### Admin (After init-prod) +``` +URL: http://localhost:8000/admin/login +Username: admin +Password: admin123 (CHANGE IN PRODUCTION!) +``` + +### Demo Vendors (After seed-demo) +``` +Vendor 1: vendor1@example.com / password123 +Vendor 2: vendor2@example.com / password123 +Vendor 3: vendor3@example.com / password123 +``` + +⚠️ **All demo passwords are INSECURE - for development only!** + +--- + +## 📊 What Each Command Creates + +### `make init-prod` +✅ Platform admin user +✅ Admin settings +✅ Role templates +✅ RBAC schema verification + +**Safe for production**: YES +**Contains fake data**: NO + +### `make seed-demo` +✅ 3 demo vendors +✅ Demo vendor users +✅ ~45 customers (15 per vendor) +✅ ~60 products (20 per vendor) +✅ Vendor themes +✅ Custom domains + +**Safe for production**: NO +**Contains fake data**: YES - ALL OF IT + +### `make seed-demo-minimal` +✅ 1 demo vendor +✅ 1 demo vendor user +✅ ~15 customers +✅ ~20 products +✅ Vendor theme +✅ Custom domain + +**Safe for production**: NO +**Contains fake data**: YES + +--- + +## 🚨 Safety Features + +### Production Warnings +```bash +# Automatically warns if: +⚠️ Using default admin password +⚠️ Using default JWT secret +⚠️ Debug mode enabled in production +⚠️ ALLOWED_HOSTS is wildcard +``` + +### Environment Protection +```bash +# seed_demo.py refuses to run if: +ENVIRONMENT=production + +Error: Cannot run demo seeding in production! +``` + +### Reset Confirmation +```bash +make seed-demo-reset +# Requires typing: DELETE ALL DATA +``` + +--- + +## 🔍 Verification Commands + +### Check Database State +```python +# Quick check +python -c " +from app.core.database import SessionLocal +from models.database.vendor import Vendor +db = SessionLocal() +print(f'Vendors: {db.query(Vendor).count()}') +db.close() +" +``` + +### View Settings +```python +# Check configuration +python -c " +from app.core.config import settings, print_environment_info +print_environment_info() +" +``` + +### Validate Production +```python +# Check production security +python -c " +from app.core.config import validate_production_settings +warnings = validate_production_settings() +if warnings: + for w in warnings: print(w) +else: + print('✅ Production configuration is secure') +" +``` + +--- + +## 🆘 Common Issues + +### "Admin already exists" +✅ **Normal!** init_production.py is idempotent (safe to run multiple times) + +### "Table doesn't exist" +❌ Run migrations first: `make migrate-up` + +### "Cannot run in production" +✅ **Correct behavior!** seed_demo.py blocks production usage + +### "Default password warning" +⚠️ Update `.env` with secure password in production + +--- + +## 📚 More Help + +```bash +make help # All commands +make help-db # Database-specific help +``` + +**Documentation**: +- `DATABASE_INIT_GUIDE.md` - Detailed guide +- `MIGRATION_GUIDE.md` - Migration from old system +- `README.md` - Project overview \ No newline at end of file diff --git a/docs/__REVAMPING/__PROJECT_ROADMAP/PRODUCT MIGRATION/product_migration_plan.md b/docs/__REVAMPING/__PROJECT_ROADMAP/PRODUCT MIGRATION/product_migration_plan.md new file mode 100644 index 00000000..aaea75ed --- /dev/null +++ b/docs/__REVAMPING/__PROJECT_ROADMAP/PRODUCT MIGRATION/product_migration_plan.md @@ -0,0 +1,1131 @@ +# Product Model Independence Migration Plan + +**Document Version:** 1.0 +**Date:** November 15, 2025 +**Status:** Proposal - Not Yet Implemented +**Priority:** Medium-High + +--- + +## Executive Summary + +This document outlines the architectural issue with the current Product model's mandatory dependency on MarketplaceProduct and proposes solutions to make the Product table independent, supporting both marketplace-imported products and vendor-created standalone products. + +--- + +## Table of Contents + +1. [Current State Analysis](#current-state-analysis) +2. [Problem Statement](#problem-statement) +3. [Use Cases](#use-cases) +4. [Proposed Solutions](#proposed-solutions) +5. [Recommended Approach](#recommended-approach) +6. [Implementation Plan](#implementation-plan) +7. [Migration Strategy](#migration-strategy) +8. [Risks and Considerations](#risks-and-considerations) +9. [Testing Strategy](#testing-strategy) +10. [Rollback Plan](#rollback-plan) + +--- + +## Current State Analysis + +### Existing Product Model Structure + +```python +# models/database/product.py (CURRENT) +class Product(Base, TimestampMixin): + __tablename__ = "products" + + id = Column(Integer, primary_key=True, index=True) + vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False) + marketplace_product_id = Column(Integer, ForeignKey("marketplace_products.id"), nullable=False) # ❌ MANDATORY + + # Vendor-specific overrides (all optional) + product_id = Column(String) # Vendor's internal SKU + price = Column(Float) + sale_price = Column(Float) + currency = Column(String) + availability = Column(String) + condition = Column(String) + + # Vendor-specific metadata + is_featured = Column(Boolean, default=False) + is_active = Column(Boolean, default=True) + display_order = Column(Integer, default=0) + + # Inventory settings + min_quantity = Column(Integer, default=1) + max_quantity = Column(Integer) +``` + +### Key Constraint + +```python +marketplace_product_id = Column(Integer, ForeignKey("marketplace_products.id"), nullable=False) +``` + +**Issue:** Every Product MUST have a corresponding MarketplaceProduct entry. + +--- + +## Problem Statement + +### The Challenge + +The current architecture **forces** all products to originate from MarketplaceProduct, creating these limitations: + +1. **Cannot create standalone products** - Vendors cannot add products directly to their catalog without first creating a MarketplaceProduct entry +2. **Artificial dependency** - Products that have no marketplace origin still require a dummy MarketplaceProduct record +3. **Data duplication** - For standalone products, we'd need to duplicate data in both tables unnecessarily +4. **Workflow complexity** - Adding a simple product requires two database operations across two tables +5. **Unclear data ownership** - When Product has override fields, which is the source of truth? + +### Business Impact + +- **Vendor friction:** More steps to add products reduces usability +- **Performance overhead:** Extra joins and table operations for simple products +- **Data integrity risks:** Duplicate data can get out of sync +- **Scalability concerns:** More tables involved in common operations + +--- + +## Use Cases + +### Use Case 1: Marketplace Import +**Scenario:** Vendor imports products from Amazon, eBay, or other marketplace +**Current Flow:** +1. Create MarketplaceProduct with all imported data +2. Create Product linking to MarketplaceProduct +3. Product has vendor-specific overrides (price, availability, etc.) + +**Works:** ✅ This use case works well with current architecture + +--- + +### Use Case 2: Standalone Product Creation +**Scenario:** Vendor manually creates a product unique to their shop +**Current Flow:** +1. ❌ Must first create dummy MarketplaceProduct +2. Create Product linking to it +3. All real data lives in Product overrides + +**Problems:** +- Unnecessary MarketplaceProduct entry +- Confusing data model +- Performance overhead + +--- + +### Use Case 3: Mixed Catalog +**Scenario:** Vendor has both imported and standalone products +**Desired State:** +- Some products linked to marketplace data +- Some products independent +- Unified vendor product management + +**Current State:** ❌ Cannot differentiate or handle differently + +--- + +## Proposed Solutions + +### Solution A: Optional Marketplace Link (Recommended) + +Make `marketplace_product_id` **nullable** and add core product fields to Product table. + +#### Schema Changes + +```python +class Product(Base, TimestampMixin): + __tablename__ = "products" + + id = Column(Integer, primary_key=True, index=True) + vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False) + + # ✅ OPTIONAL marketplace link + marketplace_product_id = Column(Integer, ForeignKey("marketplace_products.id"), nullable=True) + marketplace_source = Column(String, nullable=True) # 'amazon', 'ebay', etc. + + # Core product fields (always present) + product_id = Column(String, nullable=False, index=True) # Vendor's SKU (REQUIRED) + title = Column(String, nullable=False) # Product title (REQUIRED) + description = Column(Text, nullable=True) + + # Product identifiers + gtin = Column(String, nullable=True, index=True) # GTIN/EAN/UPC + mpn = Column(String, nullable=True) # Manufacturer Part Number + brand = Column(String, nullable=True, index=True) + + # Pricing (REQUIRED) + price = Column(Float, nullable=False) + sale_price = Column(Float, nullable=True) + currency = Column(String, default="EUR") + + # Availability + availability = Column(String, default="in stock") + condition = Column(String, default="new") + + # Media + image_link = Column(String, nullable=True) + additional_image_link = Column(Text, nullable=True) # JSON array or comma-separated + + # Categorization + google_product_category = Column(String, nullable=True, index=True) + product_type = Column(String, nullable=True) + + # Product attributes + color = Column(String, nullable=True) + size = Column(String, nullable=True) + material = Column(String, nullable=True) + gender = Column(String, nullable=True) + age_group = Column(String, nullable=True) + + # Vendor-specific metadata + is_featured = Column(Boolean, default=False) + is_active = Column(Boolean, default=True) + display_order = Column(Integer, default=0) + + # Inventory settings + min_quantity = Column(Integer, default=1) + max_quantity = Column(Integer, nullable=True) + + # Constraints + __table_args__ = ( + UniqueConstraint("vendor_id", "product_id", name="uq_vendor_product_id"), + Index("idx_product_active", "vendor_id", "is_active"), + Index("idx_product_featured", "vendor_id", "is_featured"), + Index("idx_product_marketplace", "marketplace_product_id"), + Index("idx_product_gtin", "gtin"), + ) +``` + +#### Business Logic + +```python +@property +def is_marketplace_product(self) -> bool: + """Check if product was imported from marketplace.""" + return self.marketplace_product_id is not None + +@property +def is_standalone_product(self) -> bool: + """Check if product was created directly by vendor.""" + return self.marketplace_product_id is None + +def get_effective_value(self, field_name: str): + """ + Get effective value using override strategy: + 1. Use Product field if not None + 2. Fall back to MarketplaceProduct field if linked + 3. Return None + """ + product_value = getattr(self, field_name, None) + if product_value is not None: + return product_value + + if self.marketplace_product: + return getattr(self.marketplace_product, field_name, None) + + return None +``` + +#### Pros +✅ Full independence for standalone products +✅ Maintains marketplace integration capability +✅ Clear data model with override strategy +✅ Vendors can customize imported products +✅ Backward compatible with migration + +#### Cons +⚠️ Data duplication when marketplace product is linked +⚠️ Need to maintain override logic +⚠️ Larger Product table + +--- + +### Solution B: Polymorphic Approach + +Create separate tables for marketplace and standalone products, unified through inheritance. + +#### Schema Changes + +```python +class BaseProduct(Base, TimestampMixin): + """Abstract base for all products.""" + __tablename__ = "products" + + id = Column(Integer, primary_key=True) + vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False) + product_type = Column(String, nullable=False) # 'marketplace' or 'standalone' + + # Common fields... + __mapper_args__ = { + 'polymorphic_on': product_type, + 'polymorphic_identity': 'base' + } + +class MarketplaceLinkedProduct(BaseProduct): + """Product imported from marketplace.""" + marketplace_product_id = Column(Integer, ForeignKey("marketplace_products.id")) + + __mapper_args__ = { + 'polymorphic_identity': 'marketplace' + } + +class StandaloneProduct(BaseProduct): + """Product created directly by vendor.""" + # All fields in base table + + __mapper_args__ = { + 'polymorphic_identity': 'standalone' + } +``` + +#### Pros +✅ Clean separation of concerns +✅ Type safety +✅ No null checks needed + +#### Cons +❌ Complex inheritance hierarchy +❌ Harder to query across types +❌ More difficult migration path +❌ Overkill for this use case + +--- + +### Solution C: Keep Current, Add Hybrid Flag + +Minimal change: Allow NULL marketplace_product_id but keep minimal Product fields. + +#### Schema Changes + +```python +class Product(Base, TimestampMixin): + # Same as current, but: + marketplace_product_id = Column(Integer, ForeignKey(...), nullable=True) # ✅ Made optional + + # No new fields added +``` + +When `marketplace_product_id` is NULL, all data MUST be in override fields. + +#### Pros +✅ Minimal migration +✅ Simple implementation + +#### Cons +❌ Confusing data model +❌ Unclear which fields are required +❌ Poor developer experience +❌ Validation complexity + +--- + +## Recommended Approach + +### ✅ Solution A: Optional Marketplace Link + +**Rationale:** +1. **Clear data model** - Each product is self-contained +2. **Flexible** - Supports both use cases elegantly +3. **Developer-friendly** - Easy to understand and work with +4. **Scalable** - Can add features without major refactoring +5. **Maintainable** - Override logic is straightforward + +### Data Strategy + +**For Marketplace Products:** +- Link via `marketplace_product_id` +- Product fields act as overrides +- Fall back to MarketplaceProduct for display + +**For Standalone Products:** +- `marketplace_product_id` is NULL +- All data in Product table +- No dependency on MarketplaceProduct + +--- + +## Implementation Plan + +### Phase 1: Database Migration (Week 1) + +#### Step 1.1: Create Migration File +```python +# migrations/versions/YYYYMMDD_make_product_independent.py + +def upgrade(): + # Add new columns to products table + op.add_column('products', sa.Column('title', sa.String(), nullable=True)) + op.add_column('products', sa.Column('description', sa.Text(), nullable=True)) + op.add_column('products', sa.Column('gtin', sa.String(), nullable=True)) + op.add_column('products', sa.Column('brand', sa.String(), nullable=True)) + op.add_column('products', sa.Column('image_link', sa.String(), nullable=True)) + op.add_column('products', sa.Column('google_product_category', sa.String(), nullable=True)) + op.add_column('products', sa.Column('marketplace_source', sa.String(), nullable=True)) + # ... add all other columns + + # Make marketplace_product_id nullable + op.alter_column('products', 'marketplace_product_id', nullable=True) + + # Add new indexes + op.create_index('idx_product_gtin', 'products', ['gtin']) + op.create_index('idx_product_marketplace', 'products', ['marketplace_product_id']) + + # Add unique constraint on vendor_id + product_id + op.create_unique_constraint('uq_vendor_product_id', 'products', ['vendor_id', 'product_id']) + + # Make product_id NOT NULL (after data migration) + # op.alter_column('products', 'product_id', nullable=False) # Do this in Step 1.3 + +def downgrade(): + # Reverse all changes + pass +``` + +#### Step 1.2: Data Migration - Copy Existing Data +```python +# Copy data from marketplace_products to products for existing records +from sqlalchemy import update, select + +def migrate_existing_products(): + """Copy marketplace product data to product fields.""" + + products = session.execute( + select(Product).where(Product.marketplace_product_id.isnot(None)) + ).scalars().all() + + for product in products: + if product.marketplace_product: + # Copy marketplace data to product fields if not already overridden + if not product.title: + product.title = product.marketplace_product.title + if not product.description: + product.description = product.marketplace_product.description + if not product.gtin: + product.gtin = product.marketplace_product.gtin + if not product.brand: + product.brand = product.marketplace_product.brand + # ... copy other fields + + product.marketplace_source = product.marketplace_product.marketplace + + session.commit() +``` + +#### Step 1.3: Add NOT NULL Constraints +```python +# After data migration, make required fields NOT NULL +def add_constraints(): + op.alter_column('products', 'product_id', nullable=False) + op.alter_column('products', 'title', nullable=False) + op.alter_column('products', 'price', nullable=False) +``` + +--- + +### Phase 2: Model Updates (Week 1-2) + +#### Step 2.1: Update Product Model +```python +# models/database/product.py +# Implement the full Solution A schema shown above +``` + +#### Step 2.2: Update Product Schema (Pydantic) +```python +# models/schema/product.py + +class ProductCreate(BaseModel): + # For standalone products + product_id: str = Field(..., description="Vendor's SKU") + title: str = Field(..., min_length=1) + description: Optional[str] = None + price: float = Field(..., gt=0) + gtin: Optional[str] = None + brand: Optional[str] = None + # ... other fields + + # For marketplace products + marketplace_product_id: Optional[int] = None + marketplace_source: Optional[str] = None + +class ProductUpdate(BaseModel): + title: Optional[str] = None + description: Optional[str] = None + price: Optional[float] = Field(None, gt=0) + # ... other fields +``` + +#### Step 2.3: Add Helper Methods +```python +# models/database/product.py + +class Product(Base, TimestampMixin): + # ... columns ... + + @property + def is_marketplace_product(self) -> bool: + return self.marketplace_product_id is not None + + @property + def is_standalone_product(self) -> bool: + return self.marketplace_product_id is None + + @property + def effective_title(self) -> str: + """Get title with marketplace fallback.""" + return self.title or ( + self.marketplace_product.title if self.marketplace_product else None + ) + + @property + def effective_description(self) -> Optional[str]: + """Get description with marketplace fallback.""" + return self.description or ( + self.marketplace_product.description if self.marketplace_product else None + ) + + # Add similar properties for other fields as needed +``` + +--- + +### Phase 3: Service Layer Updates (Week 2) + +#### Step 3.1: Update Product Service +```python +# services/product_service.py + +class ProductService: + + def create_standalone_product( + self, + vendor_id: int, + product_data: ProductCreate, + db: Session + ) -> Product: + """Create a standalone product (no marketplace link).""" + + # Validate vendor exists + vendor = self._get_vendor(vendor_id, db) + + # Check for duplicate product_id + existing = db.query(Product).filter( + Product.vendor_id == vendor_id, + Product.product_id == product_data.product_id + ).first() + + if existing: + raise ProductAlreadyExistsException(product_data.product_id) + + # Create product + product = Product( + vendor_id=vendor_id, + marketplace_product_id=None, # ✅ Standalone + product_id=product_data.product_id, + title=product_data.title, + description=product_data.description, + price=product_data.price, + gtin=product_data.gtin, + brand=product_data.brand, + # ... other fields + is_active=True, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc), + ) + + db.add(product) + db.commit() + db.refresh(product) + + return product + + def create_marketplace_product( + self, + vendor_id: int, + marketplace_product_id: int, + product_data: ProductCreate, + db: Session + ) -> Product: + """Link existing marketplace product to vendor catalog.""" + + # Validate marketplace product exists + mp = db.query(MarketplaceProduct).get(marketplace_product_id) + if not mp: + raise MarketplaceProductNotFoundException(marketplace_product_id) + + # Check if already linked + existing = db.query(Product).filter( + Product.vendor_id == vendor_id, + Product.marketplace_product_id == marketplace_product_id + ).first() + + if existing: + raise ProductAlreadyExistsException(f"MP-{marketplace_product_id}") + + # Create product with marketplace link + product = Product( + vendor_id=vendor_id, + marketplace_product_id=marketplace_product_id, # ✅ Linked + marketplace_source=mp.marketplace, + product_id=product_data.product_id or f"MP-{marketplace_product_id}", + # Copy/override fields from marketplace product + title=product_data.title or mp.title, + description=product_data.description or mp.description, + price=product_data.price or float(mp.price or 0), + gtin=product_data.gtin or mp.gtin, + brand=product_data.brand or mp.brand, + # ... other fields + is_active=True, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc), + ) + + db.add(product) + db.commit() + db.refresh(product) + + return product +``` + +--- + +### Phase 4: API Endpoints (Week 3) + +#### Step 4.1: Add Standalone Product Creation +```python +# routes/vendor/products.py + +@router.post("/products/standalone", response_model=ProductResponse) +async def create_standalone_product( + product_data: ProductCreate, + current_user: User = Depends(get_current_vendor_user), + db: Session = Depends(get_db) +): + """ + Create a standalone product directly in vendor catalog. + No marketplace product required. + """ + vendor = get_vendor_for_user(current_user, db) + product_service = ProductService() + + product = product_service.create_standalone_product( + vendor_id=vendor.id, + product_data=product_data, + db=db + ) + + return product + +@router.post("/products/from-marketplace", response_model=ProductResponse) +async def link_marketplace_product( + marketplace_product_id: int, + product_data: ProductCreate, + current_user: User = Depends(get_current_vendor_user), + db: Session = Depends(get_db) +): + """ + Add marketplace product to vendor catalog. + Links to existing MarketplaceProduct. + """ + vendor = get_vendor_for_user(current_user, db) + product_service = ProductService() + + product = product_service.create_marketplace_product( + vendor_id=vendor.id, + marketplace_product_id=marketplace_product_id, + product_data=product_data, + db=db + ) + + return product +``` + +--- + +### Phase 5: Frontend Updates (Week 3-4) + +#### Step 5.1: Add Product Creation UI +- Two separate forms or tabs: + 1. "Add New Product" (standalone) + 2. "Import from Marketplace" (linked) + +#### Step 5.2: Product List View +- Show badge/icon indicating product type: + - 🔗 Marketplace product + - ✏️ Standalone product + +--- + +### Phase 6: Update Seed Scripts (Week 4) + +```python +# scripts/seed_demo.py + +def create_demo_products(db: Session, vendor: Vendor, count: int): + """Create mix of standalone and marketplace products.""" + + products = [] + + for i in range(1, count + 1): + if i % 2 == 0: + # Create marketplace product + linked product + mp = MarketplaceProduct(...) + db.add(mp) + db.flush() + + product = Product( + vendor_id=vendor.id, + marketplace_product_id=mp.id, + ... + ) + else: + # Create standalone product + product = Product( + vendor_id=vendor.id, + marketplace_product_id=None, # ✅ Standalone + product_id=f"{vendor.vendor_code}-{i:03d}", + title=f"Standalone Product {i}", + ... + ) + + db.add(product) + products.append(product) + + db.commit() + return products +``` + +--- + +## Migration Strategy + +### Development Environment + +1. **Create feature branch:** `feature/product-independence` +2. **Run migrations locally** +3. **Test both product types** +4. **Update seed scripts** +5. **Run full test suite** + +### Staging Environment + +1. **Backup database** +2. **Run migrations** +3. **Verify existing products** +4. **Test new functionality** +5. **Performance testing** + +### Production Environment + +1. **Schedule maintenance window** +2. **Full database backup** +3. **Run migrations in transaction** +4. **Verify data integrity** +5. **Monitor for 24-48 hours** +6. **Rollback if issues detected** + +### Migration Checklist + +- [ ] Create database backup +- [ ] Run migration: Add nullable columns +- [ ] Data migration: Copy marketplace data to products +- [ ] Verify: All existing products have required fields +- [ ] Run migration: Add NOT NULL constraints +- [ ] Run migration: Add indexes +- [ ] Update models +- [ ] Update schemas +- [ ] Update services +- [ ] Update API endpoints +- [ ] Update frontend +- [ ] Update seed scripts +- [ ] Run full test suite +- [ ] Update documentation +- [ ] Deploy to staging +- [ ] QA testing +- [ ] Deploy to production + +--- + +## Risks and Considerations + +### Technical Risks + +| Risk | Impact | Mitigation | +|------|--------|------------| +| Data loss during migration | HIGH | Full backup, test in staging first | +| Performance degradation | MEDIUM | Add proper indexes, monitor queries | +| Breaking existing code | HIGH | Comprehensive test suite, gradual rollout | +| Null handling bugs | MEDIUM | Add validation, use helper properties | + +### Business Risks + +| Risk | Impact | Mitigation | +|------|--------|------------| +| Downtime during migration | MEDIUM | Schedule off-peak, use migration transaction | +| User confusion with new UI | LOW | Clear documentation, gradual feature release | +| Data inconsistency | HIGH | Validation in service layer, database constraints | + +### Edge Cases to Handle + +1. **Product with marketplace_product_id but no marketplace record** + - Should never happen (foreign key constraint) + - Add data validation check + +2. **Updating linked product - which fields to sync?** + - Product fields always override + - Document this clearly + +3. **Deleting marketplace product with linked products** + - Set marketplace_product_id to NULL? + - Or prevent deletion? + - **Decision needed** + +4. **Product_id uniqueness across types** + - Already handled by unique constraint + - But document the pattern + +--- + +## Testing Strategy + +### Unit Tests + +```python +# tests/test_product_independence.py + +def test_create_standalone_product(): + """Test creating product without marketplace link.""" + product = Product( + vendor_id=1, + marketplace_product_id=None, + product_id="STANDALONE-001", + title="Test Product", + price=99.99, + ) + assert product.is_standalone_product + assert not product.is_marketplace_product + +def test_create_marketplace_product(): + """Test creating product with marketplace link.""" + mp = MarketplaceProduct(...) + product = Product( + vendor_id=1, + marketplace_product_id=mp.id, + product_id="MP-001", + title="Imported Product", + price=49.99, + ) + assert product.is_marketplace_product + assert not product.is_standalone_product + +def test_effective_value_override(): + """Test that product fields override marketplace.""" + mp = MarketplaceProduct(title="Original Title") + product = Product( + marketplace_product=mp, + title="Override Title" + ) + assert product.effective_title == "Override Title" + +def test_effective_value_fallback(): + """Test fallback to marketplace when product field is None.""" + mp = MarketplaceProduct(title="Marketplace Title") + product = Product( + marketplace_product=mp, + title=None + ) + assert product.effective_title == "Marketplace Title" +``` + +### Integration Tests + +```python +def test_product_creation_flow(): + """Test full product creation via API.""" + # Test standalone + response = client.post("/api/vendor/products/standalone", json={ + "product_id": "TEST-001", + "title": "Test Product", + "price": 99.99 + }) + assert response.status_code == 201 + + # Test marketplace + mp = create_marketplace_product() + response = client.post("/api/vendor/products/from-marketplace", json={ + "marketplace_product_id": mp.id, + "product_id": "MP-TEST-001" + }) + assert response.status_code == 201 +``` + +### Migration Tests + +```python +def test_migration_preserves_data(): + """Test that migration doesn't lose data.""" + # Create products before migration + products_before = db.query(Product).all() + + # Run migration + run_alembic_migration() + + # Verify all products still exist + products_after = db.query(Product).all() + assert len(products_before) == len(products_after) + + # Verify data integrity + for product in products_after: + assert product.title is not None + assert product.price is not None +``` + +--- + +## Rollback Plan + +### If Issues Detected Within 24 Hours + +1. **Stop accepting new data** +2. **Restore database from backup** +3. **Revert code to previous version** +4. **Notify team and stakeholders** +5. **Post-mortem to identify issues** + +### Rollback Migration Script + +```python +# migrations/versions/YYYYMMDD_rollback_product_independence.py + +def downgrade(): + # Remove NOT NULL constraints + op.alter_column('products', 'product_id', nullable=True) + op.alter_column('products', 'title', nullable=True) + + # Make marketplace_product_id NOT NULL again + op.alter_column('products', 'marketplace_product_id', nullable=False) + + # Drop new columns + op.drop_column('products', 'title') + op.drop_column('products', 'description') + # ... drop all added columns + + # Drop new indexes + op.drop_index('idx_product_gtin') + op.drop_index('idx_product_marketplace') + + # Drop unique constraint + op.drop_constraint('uq_vendor_product_id') +``` + +--- + +## Post-Implementation + +### Monitoring + +- **Database query performance** - Monitor query times on products table +- **Error rates** - Watch for validation errors +- **API response times** - Track product creation/retrieval endpoints +- **User feedback** - Collect vendor feedback on new functionality + +### Documentation Updates + +- [ ] Update API documentation +- [ ] Update database schema documentation +- [ ] Create vendor user guide +- [ ] Update developer setup instructions + +### Future Enhancements + +1. **Bulk operations** - Support bulk upload of standalone products +2. **Product templates** - Allow vendors to create product templates +3. **Category management** - Better categorization for standalone products +4. **Image management** - Direct image upload for standalone products +5. **SEO optimization** - Better SEO fields for standalone products + +--- + +## Timeline Summary + +| Phase | Duration | Tasks | +|-------|----------|-------| +| Phase 1: Database Migration | 1 week | Migrations, data copy, constraints | +| Phase 2: Model Updates | 1-2 weeks | Update models, schemas, helpers | +| Phase 3: Service Layer | 1 week | Update services, validation | +| Phase 4: API Endpoints | 1 week | Add endpoints, update routes | +| Phase 5: Frontend | 1-2 weeks | Update UI, forms, displays | +| Phase 6: Testing | 1 week | Full test suite, QA | +| **Total** | **6-8 weeks** | Full implementation | + +--- + +## Decision Points + +### Decisions Needed Before Implementation + +1. **Marketplace product deletion behavior** + - [ ] Set linked products' marketplace_product_id to NULL + - [ ] Prevent deletion if products are linked + - [ ] Cascade delete (not recommended) + +2. **Product ID format requirements** + - [ ] Enforce specific format (e.g., SKU-XXXX) + - [ ] Allow any vendor format + - [ ] Auto-generate if not provided + +3. **Required vs optional fields** + - [ ] Which fields are mandatory for standalone products? + - [ ] GTIN required? (Recommended: optional) + - [ ] Brand required? (Recommended: optional) + +4. **Price synchronization** + - [ ] Allow vendors to set different price than marketplace? + - [ ] Lock price to marketplace value? + - [ ] Show both prices? + +5. **Migration timing** + - [ ] Immediate (next sprint) + - [ ] Scheduled (specific date) + - [ ] Delayed (future consideration) + +--- + +## Conclusion + +Making the Product table independent from MarketplaceProduct is a significant but necessary architectural improvement that will: + +✅ **Enable vendors to create standalone products** +✅ **Reduce unnecessary data duplication** +✅ **Improve system flexibility and scalability** +✅ **Provide clearer data ownership model** +✅ **Maintain backward compatibility with existing marketplace integration** + +**Recommended Action:** Implement Solution A (Optional Marketplace Link) following the phased approach outlined in this document. + +--- + +## Appendix + +### A. Current Constraints Summary + +```sql +-- Current Product table structure +CREATE TABLE products ( + id INTEGER PRIMARY KEY, + vendor_id INTEGER NOT NULL REFERENCES vendors(id), + marketplace_product_id INTEGER NOT NULL REFERENCES marketplace_products(id), -- ❌ MANDATORY + product_id VARCHAR, -- Nullable, should be required + price FLOAT, + sale_price FLOAT, + currency VARCHAR, + availability VARCHAR, + condition VARCHAR, + is_featured BOOLEAN DEFAULT FALSE, + is_active BOOLEAN DEFAULT TRUE, + display_order INTEGER DEFAULT 0, + min_quantity INTEGER DEFAULT 1, + max_quantity INTEGER, + created_at TIMESTAMP, + updated_at TIMESTAMP, + CONSTRAINT uq_product UNIQUE (vendor_id, marketplace_product_id) +); +``` + +### B. Proposed Constraints Summary + +```sql +-- Proposed Product table structure +CREATE TABLE products ( + id INTEGER PRIMARY KEY, + vendor_id INTEGER NOT NULL REFERENCES vendors(id), + marketplace_product_id INTEGER REFERENCES marketplace_products(id), -- ✅ NULLABLE + marketplace_source VARCHAR, + + -- Core fields (now in Product table) + product_id VARCHAR NOT NULL, -- ✅ REQUIRED + title VARCHAR NOT NULL, -- ✅ REQUIRED + description TEXT, + gtin VARCHAR, + brand VARCHAR, + price FLOAT NOT NULL, -- ✅ REQUIRED + sale_price FLOAT, + currency VARCHAR DEFAULT 'EUR', + availability VARCHAR DEFAULT 'in stock', + condition VARCHAR DEFAULT 'new', + image_link VARCHAR, + additional_image_link TEXT, + google_product_category VARCHAR, + product_type VARCHAR, + + -- Product attributes + color VARCHAR, + size VARCHAR, + material VARCHAR, + gender VARCHAR, + age_group VARCHAR, + + -- Vendor metadata + is_featured BOOLEAN DEFAULT FALSE, + is_active BOOLEAN DEFAULT TRUE, + display_order INTEGER DEFAULT 0, + min_quantity INTEGER DEFAULT 1, + max_quantity INTEGER, + + created_at TIMESTAMP, + updated_at TIMESTAMP, + + CONSTRAINT uq_vendor_product_id UNIQUE (vendor_id, product_id), + CONSTRAINT uq_vendor_marketplace_product UNIQUE (vendor_id, marketplace_product_id) +); + +CREATE INDEX idx_product_active ON products(vendor_id, is_active); +CREATE INDEX idx_product_featured ON products(vendor_id, is_featured); +CREATE INDEX idx_product_marketplace ON products(marketplace_product_id); +CREATE INDEX idx_product_gtin ON products(gtin); +CREATE INDEX idx_product_brand ON products(brand); +CREATE INDEX idx_product_category ON products(google_product_category); +``` + +### C. Related Models Reference + +```python +# MarketplaceProduct (unchanged) +class MarketplaceProduct(Base): + id = Column(Integer, primary_key=True) + marketplace_product_id = Column(String, unique=True, nullable=False) + title = Column(String, nullable=False) + description = Column(String) + price = Column(String) # Stored as string + gtin = Column(String) + brand = Column(String) + # ... many other fields + + product = relationship("Product", back_populates="marketplace_product") +``` + +### D. Configuration Settings + +```python +# app/core/config.py + +class Settings: + # Product settings + PRODUCT_ID_MIN_LENGTH: int = 3 + PRODUCT_ID_MAX_LENGTH: int = 50 + PRODUCT_TITLE_MIN_LENGTH: int = 3 + PRODUCT_TITLE_MAX_LENGTH: int = 200 + DEFAULT_CURRENCY: str = "EUR" + DEFAULT_AVAILABILITY: str = "in stock" + DEFAULT_CONDITION: str = "new" +``` + +--- + +**Document End** + +For questions or clarifications, contact the development team. \ No newline at end of file diff --git a/docs/__REVAMPING/__PROJECT_ROADMAP/PRODUCT MIGRATION/product_migration_quick_ref.md b/docs/__REVAMPING/__PROJECT_ROADMAP/PRODUCT MIGRATION/product_migration_quick_ref.md new file mode 100644 index 00000000..67ad6b07 --- /dev/null +++ b/docs/__REVAMPING/__PROJECT_ROADMAP/PRODUCT MIGRATION/product_migration_quick_ref.md @@ -0,0 +1,91 @@ +# Product Independence - Quick Reference + +## TL;DR + +**Problem:** Products must have a MarketplaceProduct entry, even for vendor-created products. + +**Solution:** Make `marketplace_product_id` nullable and add core product fields to Product table. + +**Impact:** 6-8 weeks implementation, requires database migration. + +--- + +## Current Blocker for Seed Script + +The seed script fails because it tries to create standalone products, but the current schema requires: + +```python +marketplace_product_id = Column(..., nullable=False) # ❌ MANDATORY +``` + +### Temporary Workaround (Current Seed Script) + +Create MarketplaceProduct for every Product until migration is complete: + +```python +# For each product: +# 1. Create MarketplaceProduct +# 2. Create Product linked to it +``` + +This works but violates the desired architecture. + +--- + +## Quick Decision Matrix + +| Question | Answer | Priority | +|----------|--------|----------| +| Implement now? | Not urgent - current workaround functional | Medium | +| Block for launch? | No - can ship with current architecture | N/A | +| Technical debt? | Yes - should address in 1-2 quarters | Medium-High | + +--- + +## Minimal Implementation (if needed quickly) + +**Phase 1 Only - Make nullable:** + +```python +# 1 hour migration +def upgrade(): + op.alter_column('products', 'marketplace_product_id', nullable=True) + op.add_column('products', sa.Column('title', sa.String(), nullable=True)) + op.add_column('products', sa.Column('gtin', sa.String(), nullable=True)) + # Add only critical fields + +# Updated model +class Product: + marketplace_product_id = Column(Integer, ForeignKey(...), nullable=True) # ✅ + title = Column(String, nullable=True) # Temp nullable + gtin = Column(String, nullable=True) +``` + +Then gradually add remaining fields in future migrations. + +--- + +## Key Stakeholders to Consult + +- [ ] Product Manager - Business impact, priority +- [ ] Lead Developer - Technical approach, timeline +- [ ] DevOps - Migration strategy, rollback plan +- [ ] Vendors (if beta testing) - Feature importance + +--- + +## Next Steps + +1. **Review full migration plan:** `/outputs/PRODUCT_MIGRATION_PLAN.md` +2. **Discuss with team** - Get buy-in on approach +3. **Schedule implementation** - Based on priority +4. **Create tracking ticket** - Link to migration plan + +--- + +## For Now: Use Workaround + +The updated `seed_demo.py` creates both MarketplaceProduct and Product. +This is temporary until migration is implemented. + +**No immediate action required** - continue development with current architecture. \ No newline at end of file diff --git a/make-venv.bat b/make-venv.bat deleted file mode 100644 index 66116291..00000000 --- a/make-venv.bat +++ /dev/null @@ -1,3 +0,0 @@ -@echo off -call venv\Scripts\activate.bat -C:\ProgramData\chocolatey\bin\make.exe %* \ No newline at end of file diff --git a/models/schema/team.py b/models/schema/team.py index 81db283b..ec716979 100644 --- a/models/schema/team.py +++ b/models/schema/team.py @@ -11,7 +11,7 @@ This module defines request/response schemas for: from datetime import datetime from typing import Optional, List -from pydantic import BaseModel, EmailStr, Field, validator +from pydantic import BaseModel, EmailStr, Field, field_validator # ============================================================================ @@ -72,7 +72,7 @@ class TeamMemberInvite(TeamMemberBase): description="Custom permissions (overrides role preset)" ) - @validator('role_name') + @field_validator('role_name') def validate_role_name(cls, v): """Validate role name is in allowed presets.""" if v is not None: @@ -83,7 +83,7 @@ class TeamMemberInvite(TeamMemberBase): ) return v.lower() if v else v - @validator('custom_permissions') + @field_validator('custom_permissions') def validate_custom_permissions(cls, v, values): """Ensure either role_id/role_name OR custom_permissions is provided.""" if v is not None and len(v) > 0: @@ -151,7 +151,7 @@ class InvitationAccept(BaseModel): first_name: str = Field(..., min_length=1, max_length=100) last_name: str = Field(..., min_length=1, max_length=100) - @validator('password') + @field_validator('password') def validate_password_strength(cls, v): """Validate password meets minimum requirements.""" if len(v) < 8: diff --git a/scripts/create_admin.py b/scripts/create_admin.py deleted file mode 100644 index 8a8a721f..00000000 --- a/scripts/create_admin.py +++ /dev/null @@ -1,167 +0,0 @@ -#!/usr/bin/env python3 -""" -Create default admin user for the platform. - -Usage: - python scripts/create_admin.py - -This script: -- Creates a default admin user if one doesn't exist -- Can be run multiple times safely (idempotent) -- Should be run AFTER database migrations -""" - -import sys -from pathlib import Path - -# Add project root to path -project_root = Path(__file__).parent.parent -sys.path.insert(0, str(project_root)) - -from sqlalchemy.orm import Session -from sqlalchemy import select - -from app.core.database import SessionLocal, engine -from models.database.user import User -from middleware.auth import AuthManager - -# Default admin credentials -DEFAULT_ADMIN_EMAIL = "admin@platform.com" -DEFAULT_ADMIN_USERNAME = "admin" -DEFAULT_ADMIN_PASSWORD = "admin123" # Change this in production! - - -def create_admin_user(db: Session) -> bool: - """ - Create default admin user if it doesn't exist. - - Args: - db: Database session - - Returns: - bool: True if user was created, False if already exists - """ - auth_manager = AuthManager() - - # Check if admin user already exists - existing_admin = db.execute( - select(User).where(User.username == DEFAULT_ADMIN_USERNAME) - ).scalar_one_or_none() - - if existing_admin: - print(f"ℹ️ Admin user '{DEFAULT_ADMIN_USERNAME}' already exists") - print(f" Email: {existing_admin.email}") - print(f" Role: {existing_admin.role}") - print(f" Active: {existing_admin.is_active}") - return False - - # Create new admin user - print(f"📝 Creating admin user...") - - admin_user = User( - email=DEFAULT_ADMIN_EMAIL, - username=DEFAULT_ADMIN_USERNAME, - hashed_password=auth_manager.hash_password(DEFAULT_ADMIN_PASSWORD), - role="admin", - is_active=True - ) - - db.add(admin_user) - db.commit() - db.refresh(admin_user) - - print("\n✅ Admin user created successfully!") - print("\n" + "=" * 50) - print(" Admin Credentials:") - print("=" * 50) - print(f" Email: {DEFAULT_ADMIN_EMAIL}") - print(f" Username: {DEFAULT_ADMIN_USERNAME}") - print(f" Password: {DEFAULT_ADMIN_PASSWORD}") - print("=" * 50) - print("\n⚠️ IMPORTANT: Change the password after first login!") - print(f" Login at: http://localhost:8000/static/admin/login.html") - print() - - return True - - -def verify_database_ready() -> bool: - """ - Verify that database tables exist. - - Returns: - bool: True if database is ready, False otherwise - """ - try: - # Try to query the users table - with engine.connect() as conn: - from sqlalchemy import text - result = conn.execute( - text("SELECT name FROM sqlite_master WHERE type='table' AND name='users'") - ) - tables = result.fetchall() - return len(tables) > 0 - except Exception as e: - print(f"❌ Error checking database: {e}") - return False - - -def main(): - """Main function to create admin user.""" - - print("\n" + "=" * 50) - print(" Admin User Creation Script") - print("=" * 50 + "\n") - - # Step 1: Verify database is ready - print("1️⃣ Checking database...") - - if not verify_database_ready(): - print("\n❌ ERROR: Database not ready!") - print("\n The 'users' table doesn't exist yet.") - print(" Please run database migrations first:") - print() - print(" alembic upgrade head") - print() - print(" Or if using make:") - print(" make migrate-up") - print() - sys.exit(1) - - print(" ✓ Database is ready") - - # Step 2: Create admin user - print("\n2️⃣ Creating admin user...") - - db = SessionLocal() - try: - created = create_admin_user(db) - - if created: - print("\n🎉 Setup complete! You can now:") - print(" 1. Start the server: uvicorn main:app --reload") - print(" 2. Login at: http://localhost:8000/static/admin/login.html") - print() - else: - print("\n✓ No changes needed - admin user already exists") - print() - - sys.exit(0) - - except Exception as e: - print(f"\n❌ ERROR: Failed to create admin user") - print(f" {type(e).__name__}: {e}") - print() - print(" Common issues:") - print(" - Database migrations not run") - print(" - Database connection issues") - print(" - Permissions problems") - print() - sys.exit(1) - - finally: - db.close() - - -if __name__ == "__main__": - main() diff --git a/scripts/init_production.py b/scripts/init_production.py new file mode 100644 index 00000000..6a86f412 --- /dev/null +++ b/scripts/init_production.py @@ -0,0 +1,416 @@ +#!/usr/bin/env python3 +""" +Production Database Initialization for Wizamart Platform + +This script initializes ESSENTIAL data required for production: +- Platform admin user +- Default vendor 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 pathlib import Path +from datetime import datetime, timezone + +# Add project root to path +project_root = Path(__file__).parent.parent +sys.path.insert(0, str(project_root)) + +from sqlalchemy.orm import Session +from sqlalchemy import select + +from app.core.database import SessionLocal +from app.core.config import settings, print_environment_info, validate_production_settings +from app.core.environment import is_production, get_environment +from models.database.user import User +from models.database.admin import AdminSetting +from middleware.auth import AuthManager +from app.core.permissions import PermissionGroups + + +# ============================================================================= +# 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", + first_name=settings.admin_first_name, + last_name=settings.admin_last_name, + is_active=True, + is_email_verified=True, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc), + ) + + db.add(admin) + db.flush() + + print_success(f"Created admin user: {admin.email}") + return admin + + +def create_default_role_templates(db: Session) -> dict: + """Create default role templates (not tied to any vendor). + + These serve as templates that can be copied when creating vendor-specific roles. + Note: Actual roles are vendor-specific and created when vendors 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 vendor onboarding") + + return { + "manager": list(PermissionGroups.MANAGER), + "staff": list(PermissionGroups.STAFF), + "support": list(PermissionGroups.SUPPORT), + "viewer": list(PermissionGroups.VIEWER), + "marketing": list(PermissionGroups.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_vendors_per_user", + "value": str(settings.max_vendors_per_user), + "value_type": "integer", + "description": "Maximum vendors a user can own", + "is_public": False, + }, + { + "key": "max_team_members_per_vendor", + "value": str(settings.max_team_members_per_vendor), + "value_type": "integer", + "description": "Maximum team members per vendor", + "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_vendor_verification", + "value": "true", + "value_type": "boolean", + "description": "Require admin verification for new vendors", + "is_public": False, + }, + { + "key": "allow_custom_domains", + "value": str(settings.allow_custom_domains).lower(), + "value_type": "boolean", + "description": "Allow vendors to use custom domains", + "is_public": False, + }, + { + "key": "maintenance_mode", + "value": "false", + "value_type": "boolean", + "description": "Enable maintenance mode", + "is_public": True, + }, + ] + + 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"], + description=setting_data.get("description"), + is_public=setting_data.get("is_public", False), + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.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 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 vendor_users has RBAC columns + if "vendor_users" in tables: + vu_cols = {col["name"] for col in inspector.get_columns("vendor_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 vendor_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: Set up default role templates + print_step(3, "Setting up role templates...") + role_templates = create_default_role_templates(db) + + # Step 4: Create admin settings + print_step(4, "Creating admin settings...") + create_admin_settings(db) + + # 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() + + print(f"\n📊 Database Status:") + print(f" Admin users: {user_count}") + print(f" Admin settings: {setting_count}") + + print("\n" + "─" * 70) + print("🔐 ADMIN CREDENTIALS") + print("─" * 70) + print(f" 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 vendor") + 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() diff --git a/scripts/seed_database.py b/scripts/seed_database.py deleted file mode 100644 index 7d8c7d6f..00000000 --- a/scripts/seed_database.py +++ /dev/null @@ -1,1349 +0,0 @@ -#!/usr/bin/env python3 -""" -Enhanced Database Seeder for Wizamart Platform - -Creates comprehensive test data including: -- Admin and regular users -- Multiple vendors with themes and domains -- Products and marketplace products -- Customers and addresses -- Orders and order items -- Inventory records -- Import jobs -- Admin settings and platform alerts - -Usage: - python scripts/seed_database.py [--reset] [--minimal] - - --reset : Drop all data before seeding (destructive!) - --minimal : Create only essential data (admin + 1 vendor) - -Or via Makefile: - make seed # Normal seeding - make seed-reset # Reset and seed - make seed-minimal # Minimal seeding - -This script is idempotent when run without --reset. -""" - -import sys -import argparse -from pathlib import Path -from typing import Dict, List, Tuple -from datetime import datetime, timezone, timedelta -from decimal import Decimal - -# Add project root to path -project_root = Path(__file__).parent.parent -sys.path.insert(0, str(project_root)) - -from sqlalchemy.orm import Session -from sqlalchemy import select, delete - -from app.core.database import SessionLocal, engine -from models.database.user import User -from models.database.vendor import Vendor, VendorUser, Role -from models.database.vendor_domain import VendorDomain -from models.database.vendor_theme import VendorTheme -from models.database.customer import Customer, CustomerAddress -from models.database.product import Product -from models.database.marketplace_product import MarketplaceProduct -from models.database.marketplace_import_job import MarketplaceImportJob -from models.database.order import Order, OrderItem -from models.database.admin import AdminSetting, PlatformAlert -from middleware.auth import AuthManager - -# ============================================================================= -# CONFIGURATION -# ============================================================================= - -DEFAULT_ADMIN_EMAIL = "admin@wizamart.com" -DEFAULT_ADMIN_USERNAME = "admin" -DEFAULT_ADMIN_PASSWORD = "admin123" # Change in production! - -VENDOR_CONFIGS = [ - { - "vendor_code": "WIZAMART", - "name": "WizaMart", - "subdomain": "wizamart", - "description": "Premium electronics and gadgets marketplace", - "theme_preset": "modern", - "custom_domain": "wizamart.shop", - }, - { - "vendor_code": "FASHIONHUB", - "name": "Fashion Hub", - "subdomain": "fashionhub", - "description": "Trendy clothing and accessories", - "theme_preset": "vibrant", - "custom_domain": "fashionhub.store", - }, - { - "vendor_code": "BOOKSTORE", - "name": "The Book Store", - "subdomain": "bookstore", - "description": "Books, magazines, and educational materials", - "theme_preset": "classic", - "custom_domain": None, - }, -] - -TEST_USERS = [ - { - "username": "vendor1", - "email": "vendor1@example.com", - "password": "password123", - "role": "vendor", - "first_name": "John", - "last_name": "Vendor", - }, - { - "username": "vendor2", - "email": "vendor2@example.com", - "password": "password123", - "role": "vendor", - "first_name": "Jane", - "last_name": "Merchant", - }, - { - "username": "customer1", - "email": "customer1@example.com", - "password": "password123", - "role": "customer", - "first_name": "Alice", - "last_name": "Customer", - }, -] - -THEME_PRESETS = { - "default": { - "theme_name": "default", - "colors": { - "primary": "#6366f1", - "secondary": "#8b5cf6", - "accent": "#ec4899", - "background": "#ffffff", - "text": "#1f2937", - "border": "#e5e7eb" - }, - "font_family_heading": "Inter, sans-serif", - "font_family_body": "Inter, sans-serif", - "layout_style": "grid", - "header_style": "fixed", - "product_card_style": "modern", - }, - "modern": { - "theme_name": "modern", - "colors": { - "primary": "#3b82f6", - "secondary": "#06b6d4", - "accent": "#f59e0b", - "background": "#f9fafb", - "text": "#111827", - "border": "#d1d5db" - }, - "font_family_heading": "Poppins, sans-serif", - "font_family_body": "Inter, sans-serif", - "layout_style": "grid", - "header_style": "transparent", - "product_card_style": "modern", - }, - "classic": { - "theme_name": "classic", - "colors": { - "primary": "#1e40af", - "secondary": "#7c3aed", - "accent": "#dc2626", - "background": "#ffffff", - "text": "#374151", - "border": "#e5e7eb" - }, - "font_family_heading": "Georgia, serif", - "font_family_body": "Georgia, serif", - "layout_style": "list", - "header_style": "fixed", - "product_card_style": "classic", - }, - "vibrant": { - "theme_name": "vibrant", - "colors": { - "primary": "#ec4899", - "secondary": "#f59e0b", - "accent": "#8b5cf6", - "background": "#fef3c7", - "text": "#78350f", - "border": "#fbbf24" - }, - "font_family_heading": "Montserrat, sans-serif", - "font_family_body": "Open Sans, sans-serif", - "layout_style": "masonry", - "header_style": "static", - "product_card_style": "modern", - }, - "minimal": { - "theme_name": "minimal", - "colors": { - "primary": "#000000", - "secondary": "#404040", - "accent": "#737373", - "background": "#ffffff", - "text": "#171717", - "border": "#e5e5e5" - }, - "font_family_heading": "Helvetica, Arial, sans-serif", - "font_family_body": "Helvetica, Arial, sans-serif", - "layout_style": "grid", - "header_style": "static", - "product_card_style": "minimal", - }, -} - -SAMPLE_PRODUCTS = [ - { - "marketplace_product_id": "PROD-001", - "title": "Wireless Bluetooth Headphones", - "description": "Premium noise-cancelling wireless headphones with 30-hour battery life", - "price": "149.99", - "brand": "AudioTech", - "gtin": "1234567890123", - "availability": "in stock", - "condition": "new", - "google_product_category": "Electronics > Audio > Headphones", - }, - { - "marketplace_product_id": "PROD-002", - "title": "Smart Watch Pro", - "description": "Advanced fitness tracking with heart rate monitor and GPS", - "price": "299.99", - "brand": "TechWear", - "gtin": "1234567890124", - "availability": "in stock", - "condition": "new", - "google_product_category": "Electronics > Wearables > Smart Watches", - }, - { - "marketplace_product_id": "PROD-003", - "title": "Portable Bluetooth Speaker", - "description": "Waterproof speaker with 360° sound and 12-hour battery", - "price": "79.99", - "brand": "AudioTech", - "gtin": "1234567890125", - "availability": "in stock", - "condition": "new", - "google_product_category": "Electronics > Audio > Speakers", - }, - { - "marketplace_product_id": "PROD-004", - "title": "Wireless Charging Pad", - "description": "Fast wireless charging for all Qi-enabled devices", - "price": "39.99", - "brand": "ChargeMax", - "gtin": "1234567890126", - "availability": "in stock", - "condition": "new", - "google_product_category": "Electronics > Accessories > Chargers", - }, - { - "marketplace_product_id": "PROD-005", - "title": "4K Webcam", - "description": "Ultra HD webcam with auto-focus and built-in microphone", - "price": "129.99", - "brand": "VisionTech", - "gtin": "1234567890127", - "availability": "preorder", - "condition": "new", - "google_product_category": "Electronics > Computers > Webcams", - }, -] - - -# ============================================================================= -# UTILITY FUNCTIONS -# ============================================================================= - -def print_section(title: str, char: str = "="): - """Print a formatted section header.""" - width = 70 - print(f"\n{char * width}") - print(f"{title.center(width)}") - print(f"{char * width}\n") - - -def print_step(step_num: int, description: str): - """Print a step header.""" - print(f"STEP {step_num}: {description}") - - -def print_success(message: str): - """Print a success message.""" - print(f" ✓ {message}") - - -def print_info(message: str): - """Print an info message.""" - print(f" ℹ️ {message}") - - -def print_error(message: str): - """Print an error message.""" - print(f" ✗ {message}") - - -def verify_database_ready() -> bool: - """Verify that database tables exist.""" - try: - with engine.connect() as conn: - from sqlalchemy import text - - required_tables = ['users', 'vendors', 'products', 'marketplace_products'] - - for table in required_tables: - result = conn.execute( - text(f"SELECT name FROM sqlite_master WHERE type='table' AND name='{table}'") - ) - if not result.fetchall(): - print_error(f"Required table '{table}' not found") - return False - - return True - - except Exception as e: - print_error(f"Error checking database: {e}") - return False - - -def reset_database(db: Session): - """Delete all data from database (destructive!).""" - print_section("RESETTING DATABASE", "!") - print("⚠️ WARNING: This will delete ALL data!") - - # Order matters due to foreign key constraints - tables_to_clear = [ - OrderItem, - Order, - CustomerAddress, - Customer, - Product, - MarketplaceProduct, - MarketplaceImportJob, - VendorUser, - Role, - VendorDomain, - VendorTheme, - Vendor, - AdminSetting, - PlatformAlert, - User, - ] - - try: - for model in tables_to_clear: - count = db.query(model).count() - if count > 0: - db.execute(delete(model)) - print_info(f"Deleted {count} records from {model.__tablename__}") - - db.commit() - print_success("Database reset complete") - - except Exception as e: - db.rollback() - print_error(f"Failed to reset database: {e}") - raise - - -# ============================================================================= -# SEEDING FUNCTIONS -# ============================================================================= - -def create_user( - db: Session, - auth_manager: AuthManager, - username: str, - email: str, - password: str, - role: str = "user", - first_name: str = None, - last_name: str = None, -) -> Tuple[User, bool]: - """Create user if it doesn't exist.""" - - existing_user = db.execute( - select(User).where(User.username == username) - ).scalar_one_or_none() - - if existing_user: - return existing_user, False - - user = User( - email=email, - username=username, - hashed_password=auth_manager.hash_password(password), - role=role, - first_name=first_name, - last_name=last_name, - is_active=True, - created_at=datetime.now(timezone.utc), - updated_at=datetime.now(timezone.utc) - ) - - db.add(user) - db.flush() - - return user, True - - -def create_vendor( - db: Session, - vendor_code: str, - name: str, - subdomain: str, - owner_user_id: int, - description: str = None, -) -> Tuple[Vendor, bool]: - """Create vendor if it doesn't exist.""" - - existing_vendor = db.execute( - select(Vendor).where(Vendor.vendor_code == vendor_code) - ).scalar_one_or_none() - - if existing_vendor: - return existing_vendor, False - - vendor = Vendor( - vendor_code=vendor_code, - subdomain=subdomain, - name=name, - description=description or f"{name} - Online marketplace", - owner_user_id=owner_user_id, - contact_email=f"contact@{subdomain}.com", - contact_phone="+352 123 456 789", - website=f"https://{subdomain}.com", - is_active=True, - is_verified=True, - created_at=datetime.now(timezone.utc), - updated_at=datetime.now(timezone.utc) - ) - - db.add(vendor) - db.flush() - - return vendor, True - - -def create_vendor_theme( - db: Session, - vendor_id: int, - theme_preset: str = "default", -) -> Tuple[VendorTheme, bool]: - """Create vendor theme if it doesn't exist.""" - - existing_theme = db.execute( - select(VendorTheme).where(VendorTheme.vendor_id == vendor_id) - ).scalar_one_or_none() - - if existing_theme: - return existing_theme, False - - preset = THEME_PRESETS.get(theme_preset, THEME_PRESETS["default"]) - - theme = VendorTheme( - vendor_id=vendor_id, - theme_name=preset["theme_name"], - is_active=True, - colors=preset["colors"], - font_family_heading=preset["font_family_heading"], - font_family_body=preset["font_family_body"], - layout_style=preset["layout_style"], - header_style=preset["header_style"], - product_card_style=preset["product_card_style"], - social_links={ - "facebook": f"https://facebook.com/vendor{vendor_id}", - "instagram": f"https://instagram.com/vendor{vendor_id}", - "twitter": f"https://twitter.com/vendor{vendor_id}", - }, - created_at=datetime.now(timezone.utc), - updated_at=datetime.now(timezone.utc) - ) - - db.add(theme) - db.flush() - - return theme, True - - -def create_vendor_domain( - db: Session, - vendor_id: int, - domain: str, - is_primary: bool = False, -) -> Tuple[VendorDomain, bool]: - """Create vendor custom domain if it doesn't exist.""" - - normalized_domain = VendorDomain.normalize_domain(domain) - - existing_domain = db.execute( - select(VendorDomain).where(VendorDomain.domain == normalized_domain) - ).scalar_one_or_none() - - if existing_domain: - return existing_domain, False - - vendor_domain = VendorDomain( - vendor_id=vendor_id, - domain=normalized_domain, - is_primary=is_primary, - is_active=True, - ssl_status="pending", - is_verified=False, - verification_token=f"verify_{vendor_id}_{normalized_domain.replace('.', '_')}", - created_at=datetime.now(timezone.utc), - updated_at=datetime.now(timezone.utc) - ) - - db.add(vendor_domain) - db.flush() - - return vendor_domain, True - - -def create_marketplace_product( - db: Session, - product_data: Dict, - marketplace: str = "Wizamart", - vendor_name: str = None, -) -> Tuple[MarketplaceProduct, bool]: - """Create marketplace product if it doesn't exist.""" - - existing_product = db.execute( - select(MarketplaceProduct).where( - MarketplaceProduct.marketplace_product_id == product_data["marketplace_product_id"] - ) - ).scalar_one_or_none() - - if existing_product: - return existing_product, False - - mp_product = MarketplaceProduct( - marketplace_product_id=product_data["marketplace_product_id"], - title=product_data["title"], - description=product_data.get("description"), - price=product_data.get("price"), - brand=product_data.get("brand"), - gtin=product_data.get("gtin"), - availability=product_data.get("availability", "in stock"), - condition=product_data.get("condition", "new"), - google_product_category=product_data.get("google_product_category"), - marketplace=marketplace, - vendor_name=vendor_name, - currency="EUR", - created_at=datetime.now(timezone.utc), - updated_at=datetime.now(timezone.utc) - ) - - db.add(mp_product) - db.flush() - - return mp_product, True - - -def create_vendor_product( - db: Session, - vendor_id: int, - marketplace_product_id: int, - product_id: str = None, - price: float = None, - is_featured: bool = False, -) -> Tuple[Product, bool]: - """Create vendor product (links vendor to marketplace product).""" - - existing_product = db.execute( - select(Product).where( - Product.vendor_id == vendor_id, - Product.marketplace_product_id == marketplace_product_id - ) - ).scalar_one_or_none() - - if existing_product: - return existing_product, False - - product = Product( - vendor_id=vendor_id, - marketplace_product_id=marketplace_product_id, - product_id=product_id, - price=price, - currency="EUR", - availability="in stock", - condition="new", - is_featured=is_featured, - is_active=True, - display_order=0, - min_quantity=1, - created_at=datetime.now(timezone.utc), - updated_at=datetime.now(timezone.utc) - ) - - db.add(product) - db.flush() - - return product, True - - -def create_customer( - db: Session, - auth_manager: AuthManager, - vendor_id: int, - email: str, - password: str, - first_name: str, - last_name: str, - customer_number: str, -) -> Tuple[Customer, bool]: - """Create customer for a vendor.""" - - existing_customer = db.execute( - select(Customer).where( - Customer.vendor_id == vendor_id, - Customer.email == email - ) - ).scalar_one_or_none() - - if existing_customer: - return existing_customer, False - - customer = Customer( - vendor_id=vendor_id, - email=email, - hashed_password=auth_manager.hash_password(password), - first_name=first_name, - last_name=last_name, - phone="+352 99 123 456", - customer_number=customer_number, - is_active=True, - total_orders=0, - total_spent=Decimal("0.00"), - marketing_consent=True, - created_at=datetime.now(timezone.utc), - updated_at=datetime.now(timezone.utc) - ) - - db.add(customer) - db.flush() - - return customer, True - - -def create_customer_address( - db: Session, - vendor_id: int, - customer_id: int, - address_type: str, - is_default: bool = False, -) -> Tuple[CustomerAddress, bool]: - """Create customer address.""" - - existing_address = db.execute( - select(CustomerAddress).where( - CustomerAddress.customer_id == customer_id, - CustomerAddress.address_type == address_type - ) - ).scalar_one_or_none() - - if existing_address: - return existing_address, False - - address = CustomerAddress( - vendor_id=vendor_id, - customer_id=customer_id, - address_type=address_type, - first_name="John", - last_name="Doe", - company="ACME Corp" if address_type == "billing" else None, - address_line_1="123 Main Street", - address_line_2="Apt 4B", - city="Luxembourg", - postal_code="L-1234", - country="Luxembourg", - is_default=is_default, - created_at=datetime.now(timezone.utc), - updated_at=datetime.now(timezone.utc) - ) - - db.add(address) - db.flush() - - return address, True - - -def create_order( - db: Session, - vendor_id: int, - customer_id: int, - order_number: str, - products: List[Tuple[int, int, float]], # (product_id, quantity, price) - shipping_address_id: int, - billing_address_id: int, - status: str = "pending", -) -> Tuple[Order, bool]: - """Create order with items.""" - - existing_order = db.execute( - select(Order).where(Order.order_number == order_number) - ).scalar_one_or_none() - - if existing_order: - return existing_order, False - - # Calculate totals - subtotal = sum(qty * price for _, qty, price in products) - tax_amount = subtotal * 0.17 # 17% VAT - shipping_amount = 9.99 if subtotal < 50 else 0.0 - total_amount = subtotal + tax_amount + shipping_amount - - order = Order( - vendor_id=vendor_id, - customer_id=customer_id, - order_number=order_number, - status=status, - subtotal=subtotal, - tax_amount=tax_amount, - shipping_amount=shipping_amount, - discount_amount=0.0, - total_amount=total_amount, - currency="EUR", - shipping_address_id=shipping_address_id, - billing_address_id=billing_address_id, - shipping_method="Standard Shipping", - created_at=datetime.now(timezone.utc), - updated_at=datetime.now(timezone.utc) - ) - - db.add(order) - db.flush() - - # Create order items - for product_id, quantity, unit_price in products: - order_item = OrderItem( - order_id=order.id, - product_id=product_id, - product_name=f"Product {product_id}", - product_sku=f"SKU-{product_id}", - quantity=quantity, - unit_price=unit_price, - total_price=quantity * unit_price, - inventory_reserved=True, - inventory_fulfilled=False, - created_at=datetime.now(timezone.utc), - updated_at=datetime.now(timezone.utc) - ) - db.add(order_item) - - db.flush() - return order, True - - -def create_import_job( - db: Session, - vendor_id: int, - user_id: int, - marketplace: str, - status: str = "completed", - imported_count: int = 5, -) -> Tuple[MarketplaceImportJob, bool]: - """Create import job.""" - - job = MarketplaceImportJob( - vendor_id=vendor_id, - user_id=user_id, - marketplace=marketplace, - source_url=f"https://{marketplace.lower()}.com/feed.xml", - status=status, - imported_count=imported_count, - updated_count=0, - error_count=0, - total_processed=imported_count, - started_at=datetime.now(timezone.utc) - timedelta(hours=1), - completed_at=datetime.now(timezone.utc), - created_at=datetime.now(timezone.utc), - updated_at=datetime.now(timezone.utc) - ) - - db.add(job) - db.flush() - - return job, True - - -def create_admin_settings(db: Session) -> List[AdminSetting]: - """Create platform admin settings.""" - - settings = [ - { - "key": "max_vendors_allowed", - "value": "1000", - "value_type": "integer", - "category": "system", - "description": "Maximum number of vendors allowed on the platform", - }, - { - "key": "maintenance_mode", - "value": "false", - "value_type": "boolean", - "category": "system", - "description": "Enable maintenance mode to prevent access", - }, - { - "key": "default_vendor_trial_days", - "value": "30", - "value_type": "integer", - "category": "marketplace", - "description": "Default trial period for new vendors in days", - }, - { - "key": "platform_commission_rate", - "value": "0.05", - "value_type": "float", - "category": "marketplace", - "description": "Platform commission rate (5%)", - }, - ] - - created_settings = [] - - for setting_data in settings: - existing_setting = db.execute( - select(AdminSetting).where(AdminSetting.key == setting_data["key"]) - ).scalar_one_or_none() - - if not existing_setting: - setting = AdminSetting( - key=setting_data["key"], - value=setting_data["value"], - value_type=setting_data["value_type"], - category=setting_data["category"], - description=setting_data["description"], - is_encrypted=False, - is_public=False, - created_at=datetime.now(timezone.utc), - updated_at=datetime.now(timezone.utc) - ) - db.add(setting) - created_settings.append(setting) - - db.flush() - return created_settings - - -def create_platform_alerts(db: Session) -> List[PlatformAlert]: - """Create sample platform alerts.""" - - alerts = [ - { - "alert_type": "performance", - "severity": "info", - "title": "High Traffic Detected", - "description": "Platform experiencing higher than normal traffic", - "is_resolved": True, - "resolved_at": datetime.now(timezone.utc) - timedelta(hours=2), - }, - { - "alert_type": "security", - "severity": "warning", - "title": "Multiple Failed Login Attempts", - "description": "Detected multiple failed login attempts from IP 192.168.1.100", - "is_resolved": False, - }, - ] - - created_alerts = [] - - for alert_data in alerts: - alert = PlatformAlert( - alert_type=alert_data["alert_type"], - severity=alert_data["severity"], - title=alert_data["title"], - description=alert_data["description"], - is_resolved=alert_data["is_resolved"], - resolved_at=alert_data.get("resolved_at"), - auto_generated=True, - occurrence_count=1, - first_occurred_at=datetime.now(timezone.utc) - timedelta(hours=3), - last_occurred_at=datetime.now(timezone.utc), - created_at=datetime.now(timezone.utc), - updated_at=datetime.now(timezone.utc) - ) - db.add(alert) - created_alerts.append(alert) - - db.flush() - return created_alerts - - -# ============================================================================= -# MAIN SEEDING LOGIC -# ============================================================================= - -def seed_minimal(db: Session, auth_manager: AuthManager): - """Seed minimal data (admin + 1 vendor).""" - - print_section("MINIMAL SEEDING") - - # Create admin user - print_step(1, "Creating admin user...") - admin, admin_created = create_user( - db, auth_manager, - username=DEFAULT_ADMIN_USERNAME, - email=DEFAULT_ADMIN_EMAIL, - password=DEFAULT_ADMIN_PASSWORD, - role="admin" - ) - - if admin_created: - print_success(f"Admin user created (ID: {admin.id})") - else: - print_info(f"Admin user already exists (ID: {admin.id})") - - # Create WizaMart vendor - print_step(2, "Creating WizaMart vendor...") - vendor, vendor_created = create_vendor( - db, - vendor_code="WIZAMART", - name="WizaMart", - subdomain="wizamart", - owner_user_id=admin.id, - description="Premium electronics marketplace" - ) - - if vendor_created: - print_success(f"WizaMart created (ID: {vendor.id})") - else: - print_info(f"WizaMart already exists (ID: {vendor.id})") - - db.commit() - print_success("Minimal seeding complete") - - -def seed_full(db: Session, auth_manager: AuthManager): - """Seed comprehensive test data.""" - - print_section("COMPREHENSIVE SEEDING") - - # ========================================================================= - # STEP 1: CREATE USERS - # ========================================================================= - - print_step(1, "Creating users...") - - # Create admin - admin, admin_created = create_user( - db, auth_manager, - username=DEFAULT_ADMIN_USERNAME, - email=DEFAULT_ADMIN_EMAIL, - password=DEFAULT_ADMIN_PASSWORD, - role="admin" - ) - - if admin_created: - print_success(f"Admin user created (ID: {admin.id})") - else: - print_info(f"Admin user already exists (ID: {admin.id})") - - # Create test users - test_users_created = [] - for user_data in TEST_USERS: - user, created = create_user( - db, auth_manager, - username=user_data["username"], - email=user_data["email"], - password=user_data["password"], - role=user_data["role"], - first_name=user_data.get("first_name"), - last_name=user_data.get("last_name"), - ) - if created: - test_users_created.append(user) - print_success(f"User '{user.username}' created (ID: {user.id})") - - if not test_users_created: - print_info("All test users already exist") - - # ========================================================================= - # STEP 2: CREATE VENDORS - # ========================================================================= - - print_step(2, "Creating vendors...") - - vendors = [] - for config in VENDOR_CONFIGS: - vendor, created = create_vendor( - db, - vendor_code=config["vendor_code"], - name=config["name"], - subdomain=config["subdomain"], - owner_user_id=admin.id, - description=config["description"] - ) - vendors.append(vendor) - - if created: - print_success(f"{config['vendor_code']} created (ID: {vendor.id})") - - # Create vendor theme - theme, theme_created = create_vendor_theme( - db, - vendor_id=vendor.id, - theme_preset=config.get("theme_preset", "default") - ) - if theme_created: - print_success(f" Theme '{theme.theme_name}' applied") - - # Create custom domain if specified - if config.get("custom_domain"): - domain, domain_created = create_vendor_domain( - db, - vendor_id=vendor.id, - domain=config["custom_domain"], - is_primary=True - ) - if domain_created: - print_success(f" Custom domain '{domain.domain}' added") - else: - print_info(f"{config['vendor_code']} already exists (ID: {vendor.id})") - - # ========================================================================= - # STEP 3: CREATE PRODUCTS - # ========================================================================= - - print_step(3, "Creating products...") - - products_created = 0 - vendor_products = [] - - for product_data in SAMPLE_PRODUCTS: - # Create marketplace product - mp_product, created = create_marketplace_product( - db, - product_data=product_data, - marketplace="Wizamart", - vendor_name=vendors[0].name - ) - - if created: - products_created += 1 - - # Link to first two vendors - for vendor in vendors[:2]: - vendor_product, vp_created = create_vendor_product( - db, - vendor_id=vendor.id, - marketplace_product_id=mp_product.id, - product_id=f"{vendor.vendor_code}-{product_data['marketplace_product_id']}", - price=float(product_data["price"]), - is_featured=(products_created <= 2) - ) - if vp_created: - vendor_products.append(vendor_product) - - print_success(f"Created {products_created} marketplace products") - print_success(f"Created {len(vendor_products)} vendor product links") - - # ========================================================================= - # STEP 4: CREATE CUSTOMERS - # ========================================================================= - - print_step(4, "Creating customers...") - - customers_created = 0 - addresses_created = 0 - - for i, vendor in enumerate(vendors[:2]): - # Create 2 customers per vendor - for j in range(1, 3): - customer, created = create_customer( - db, - auth_manager, - vendor_id=vendor.id, - email=f"customer{i + 1}{j}@example.com", - password="password123", - first_name=f"Customer{i + 1}{j}", - last_name="Buyer", - customer_number=f"{vendor.vendor_code}-CUST-{j:04d}" - ) - - if created: - customers_created += 1 - - # Create addresses - shipping_addr, _ = create_customer_address( - db, vendor.id, customer.id, "shipping", is_default=True - ) - billing_addr, _ = create_customer_address( - db, vendor.id, customer.id, "billing", is_default=True - ) - addresses_created += 2 - - print_success(f"Created {customers_created} customers") - print_success(f"Created {addresses_created} addresses") - - # ========================================================================= - # STEP 5: CREATE ORDERS - # ========================================================================= - - print_step(5, "Creating orders...") - - orders_created = 0 - - # Get customers for first vendor - vendor = vendors[0] - vendor_customers = db.execute( - select(Customer).where(Customer.vendor_id == vendor.id) - ).scalars().all() - - vendor_product_list = [vp for vp in vendor_products if vp.vendor_id == vendor.id] - - if vendor_customers and vendor_product_list: - for i, customer in enumerate(vendor_customers[:2]): - # Get customer addresses - addresses = db.execute( - select(CustomerAddress).where(CustomerAddress.customer_id == customer.id) - ).scalars().all() - - if len(addresses) >= 2: - shipping_addr = next(a for a in addresses if a.address_type == "shipping") - billing_addr = next(a for a in addresses if a.address_type == "billing") - - # Create order with 2 products - products_for_order = [ - (vendor_product_list[0].id, 1, 149.99), - (vendor_product_list[1].id, 2, 79.99), - ] - - order, created = create_order( - db, - vendor_id=vendor.id, - customer_id=customer.id, - order_number=f"ORD-{vendor.vendor_code}-{i + 1:05d}", - products=products_for_order, - shipping_address_id=shipping_addr.id, - billing_address_id=billing_addr.id, - status="completed" if i == 0 else "processing" - ) - - if created: - orders_created += 1 - - print_success(f"Created {orders_created} orders") - - # ========================================================================= - # STEP 6: CREATE IMPORT JOBS - # ========================================================================= - - print_step(6, "Creating import jobs...") - - jobs_created = 0 - - for vendor in vendors[:2]: - job, created = create_import_job( - db, - vendor_id=vendor.id, - user_id=admin.id, - marketplace="Wizamart", - status="completed", - imported_count=len(SAMPLE_PRODUCTS) - ) - if created: - jobs_created += 1 - - print_success(f"Created {jobs_created} import jobs") - - # ========================================================================= - # STEP 7: CREATE ADMIN DATA - # ========================================================================= - - print_step(7, "Creating admin settings and alerts...") - - settings = create_admin_settings(db) - alerts = create_platform_alerts(db) - - print_success(f"Created {len(settings)} admin settings") - print_success(f"Created {len(alerts)} platform alerts") - - # ========================================================================= - # COMMIT ALL CHANGES - # ========================================================================= - - db.commit() - print_success("All data committed successfully") - - -def print_summary(db: Session): - """Print summary of seeded data.""" - - print_section("SEEDING SUMMARY") - - # Count records - user_count = db.query(User).count() - vendor_count = db.query(Vendor).count() - theme_count = db.query(VendorTheme).count() - domain_count = db.query(VendorDomain).count() - mp_product_count = db.query(MarketplaceProduct).count() - product_count = db.query(Product).count() - customer_count = db.query(Customer).count() - address_count = db.query(CustomerAddress).count() - order_count = db.query(Order).count() - order_item_count = db.query(OrderItem).count() - import_job_count = db.query(MarketplaceImportJob).count() - setting_count = db.query(AdminSetting).count() - alert_count = db.query(PlatformAlert).count() - - print(f"📊 Database Statistics:") - print(f" Users: {user_count}") - print(f" Vendors: {vendor_count}") - print(f" Vendor Themes: {theme_count}") - print(f" Custom Domains: {domain_count}") - print(f" Marketplace Products: {mp_product_count}") - print(f" Vendor Products: {product_count}") - print(f" Customers: {customer_count}") - print(f" Addresses: {address_count}") - print(f" Orders: {order_count}") - print(f" Order Items: {order_item_count}") - print(f" Import Jobs: {import_job_count}") - print(f" Admin Settings: {setting_count}") - print(f" Platform Alerts: {alert_count}") - - print("\n" + "─" * 70) - print("🔐 ADMIN LOGIN CREDENTIALS") - print("─" * 70) - print(f" URL: http://localhost:8000/admin/login") - print(f" Username: {DEFAULT_ADMIN_USERNAME}") - print(f" Password: {DEFAULT_ADMIN_PASSWORD}") - print("─" * 70) - - print("\n" + "─" * 70) - print("🏪 VENDORS") - print("─" * 70) - - vendors = db.execute(select(Vendor)).scalars().all() - for vendor in vendors: - print(f"\n {vendor.vendor_code}") - print(f" Name: {vendor.name}") - print(f" Subdomain: {vendor.subdomain}") - - if vendor.vendor_theme: - print(f" Theme: {vendor.vendor_theme.theme_name}") - - custom_domains = [d for d in vendor.domains if d.is_active] - if custom_domains: - print(f" Domains: {', '.join(d.domain for d in custom_domains)}") - - product_count = db.query(Product).filter(Product.vendor_id == vendor.id).count() - print(f" Products: {product_count}") - - customer_count = db.query(Customer).filter(Customer.vendor_id == vendor.id).count() - print(f" Customers: {customer_count}") - - print("\n" + "─" * 70) - print("🚀 NEXT STEPS") - print("─" * 70) - print(" 1. Start the server:") - print(" make dev") - print() - print(" 2. Login to admin panel:") - print(" http://localhost:8000/admin/login") - print() - print(" 3. Test vendor shops:") - for vendor in vendors[:3]: - print(f" http://localhost:8000/shop/{vendor.vendor_code}") - print() - print("⚠️ SECURITY: Change default passwords in production!") - print() - - -# ============================================================================= -# MAIN ENTRY POINT -# ============================================================================= - -def main(): - """Main entry point for database seeding.""" - - parser = argparse.ArgumentParser( - description="Seed Wizamart database with test data" - ) - parser.add_argument( - "--reset", - action="store_true", - help="Reset database before seeding (destructive!)" - ) - parser.add_argument( - "--minimal", - action="store_true", - help="Create only minimal data (admin + 1 vendor)" - ) - - args = parser.parse_args() - - print("\n" + "╔" + "═" * 68 + "╗") - print("║" + " " * 18 + "WIZAMART DATABASE SEEDER" + " " * 26 + "║") - print("╚" + "═" * 68 + "╝\n") - - # ========================================================================= - # VERIFY DATABASE - # ========================================================================= - - print_step(1, "Verifying database...") - - if not verify_database_ready(): - print_error("Database not ready!") - print("\nRequired tables don't exist. Run migrations first:") - print(" make migrate-up\n") - sys.exit(1) - - print_success("Database tables verified") - - # ========================================================================= - # SEED DATA - # ========================================================================= - - db = SessionLocal() - auth_manager = AuthManager() - - try: - # Reset database if requested - if args.reset: - confirm = input("\n⚠️ Are you sure you want to DELETE ALL DATA? (type 'yes' to confirm): ") - if confirm.lower() == 'yes': - reset_database(db) - else: - print("Reset cancelled.") - return - - # Seed data - if args.minimal: - seed_minimal(db, auth_manager) - else: - seed_full(db, auth_manager) - - # Print summary - print_summary(db) - - print_section("✅ SEEDING COMPLETED SUCCESSFULLY!", "=") - - except KeyboardInterrupt: - db.rollback() - print("\n\n⚠️ Seeding interrupted by user") - sys.exit(1) - - except Exception as e: - db.rollback() - print_section("❌ SEEDING FAILED", "!") - print(f"Error: {e}\n") - import traceback - traceback.print_exc() - sys.exit(1) - - finally: - db.close() - - -if __name__ == "__main__": - main() diff --git a/scripts/seed_demo.py b/scripts/seed_demo.py new file mode 100644 index 00000000..6e224ea7 --- /dev/null +++ b/scripts/seed_demo.py @@ -0,0 +1,658 @@ +#!/usr/bin/env python3 +""" +Demo Database Seeder for Wizamart Platform + +Creates DEMO/TEST data for development and testing: +- Demo vendors with realistic data +- Test customers and addresses +- Sample products +- Demo orders +- Vendor 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 vendor only) + make seed-demo-reset # Delete all data and reseed (DANGEROUS!) + +This script is idempotent when run normally. +""" + +import sys +from pathlib import Path +from typing import Dict, List +from datetime import datetime, timezone, timedelta +from decimal import Decimal + +# Add project root to path +project_root = Path(__file__).parent.parent +sys.path.insert(0, str(project_root)) + +from sqlalchemy.orm import Session +from sqlalchemy import select, delete + +from app.core.database import SessionLocal +from app.core.config import settings +from app.core.environment import is_production, get_environment +from models.database.user import User +from models.database.vendor import Vendor, VendorUser, Role +from models.database.vendor_domain import VendorDomain +from models.database.vendor_theme import VendorTheme +from models.database.customer import Customer, CustomerAddress +from models.database.product import Product +from models.database.marketplace_product import MarketplaceProduct +from models.database.marketplace_import_job import MarketplaceImportJob +from models.database.order import Order, OrderItem +from models.database.admin import PlatformAlert +from middleware.auth import AuthManager + +# ============================================================================= +# MODE DETECTION (from environment variable set by Makefile) +# ============================================================================= +import os + +SEED_MODE = os.getenv('SEED_MODE', 'normal') # normal, minimal, reset + +# ============================================================================= +# DEMO DATA CONFIGURATION +# ============================================================================= + +# Demo vendor configurations +DEMO_VENDORS = [ + { + "vendor_code": "WIZAMART", + "name": "WizaMart", + "subdomain": "wizamart", + "description": "Premium electronics and gadgets marketplace", + "theme_preset": "modern", + "custom_domain": "wizamart.shop", + }, + { + "vendor_code": "FASHIONHUB", + "name": "Fashion Hub", + "subdomain": "fashionhub", + "description": "Trendy clothing and accessories", + "theme_preset": "vibrant", + "custom_domain": "fashionhub.store", + }, + { + "vendor_code": "BOOKSTORE", + "name": "The Book Store", + "subdomain": "bookstore", + "description": "Books, magazines, and educational materials", + "theme_preset": "classic", + "custom_domain": None, + }, +] + +# Demo users (vendor owners) +DEMO_VENDOR_USERS = [ + { + "username": "vendor1", + "email": "vendor1@example.com", + "password": "password123", + "first_name": "John", + "last_name": "Vendor", + }, + { + "username": "vendor2", + "email": "vendor2@example.com", + "password": "password123", + "first_name": "Jane", + "last_name": "Merchant", + }, + { + "username": "vendor3", + "email": "vendor3@example.com", + "password": "password123", + "first_name": "Bob", + "last_name": "Seller", + }, +] + +# 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", + }, +} + + +# ============================================================================= +# 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") + ).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 vendors, customers, orders, etc.") + print(" Admin user will be preserved.") + + # Get confirmation + response = input("\n Type 'DELETE ALL DATA' to confirm: ") + if response != "DELETE ALL DATA": + print(" Reset cancelled.") + sys.exit(0) + + # Delete in correct order (respecting foreign keys) + tables_to_clear = [ + OrderItem, + Order, + CustomerAddress, + Customer, + MarketplaceImportJob, + MarketplaceProduct, + Product, + VendorDomain, + VendorTheme, + Role, + VendorUser, + Vendor, + PlatformAlert, + ] + + for table in tables_to_clear: + 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_vendors(db: Session, auth_manager: AuthManager) -> List[Vendor]: + """Create demo vendors with users.""" + + vendors = [] + + # Determine how many vendors to create based on mode + vendor_count = 1 if SEED_MODE == 'minimal' else settings.seed_demo_vendors + vendors_to_create = DEMO_VENDORS[:vendor_count] + users_to_create = DEMO_VENDOR_USERS[:vendor_count] + + for vendor_data, user_data in zip(vendors_to_create, users_to_create): + # Check if vendor already exists + existing = db.execute( + select(Vendor).where(Vendor.vendor_code == vendor_data["vendor_code"]) + ).scalar_one_or_none() + + if existing: + print_warning(f"Vendor already exists: {vendor_data['name']}") + vendors.append(existing) + continue + + # Create vendor user + vendor_user = User( + username=user_data["username"], + email=user_data["email"], + hashed_password=auth_manager.hash_password(user_data["password"]), + role="vendor", + first_name=user_data["first_name"], + last_name=user_data["last_name"], + is_active=True, + is_email_verified=True, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc), + ) + db.add(vendor_user) + db.flush() + + # Create vendor + vendor = Vendor( + vendor_code=vendor_data["vendor_code"], + name=vendor_data["name"], + subdomain=vendor_data["subdomain"], + description=vendor_data["description"], + is_active=True, + is_verified=True, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc), + ) + db.add(vendor) + db.flush() + + # Link user to vendor as owner + vendor_user_link = VendorUser( + vendor_id=vendor.id, + user_id=vendor_user.id, + user_type="owner", + is_active=True, + created_at=datetime.now(timezone.utc), + ) + db.add(vendor_user_link) + + # Create vendor theme + theme_colors = THEME_PRESETS.get(vendor_data["theme_preset"], THEME_PRESETS["modern"]) + theme = VendorTheme( + vendor_id=vendor.id, + theme_name=vendor_data["theme_preset"], + primary_color=theme_colors["primary"], + secondary_color=theme_colors["secondary"], + accent_color=theme_colors["accent"], + background_color=theme_colors["background"], + text_color=theme_colors["text"], + is_active=True, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc), + ) + db.add(theme) + + # Create custom domain if specified + if vendor_data.get("custom_domain"): + domain = VendorDomain( + vendor_id=vendor.id, + domain_name=vendor_data["custom_domain"], + is_verified=True, # Auto-verified for demo + is_active=True, + verification_token=None, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc), + ) + db.add(domain) + + vendors.append(vendor) + print_success(f"Created vendor: {vendor.name} ({vendor.vendor_code})") + + db.flush() + return vendors + + +def create_demo_customers(db: Session, vendor: Vendor, auth_manager: AuthManager, count: int) -> List[Customer]: + """Create demo customers for a vendor.""" + + customers = [] + # Use a simple demo password for all customers + demo_password = "customer123" + + for i in range(1, count + 1): + email = f"customer{i}@{vendor.subdomain}.example.com" + customer_number = f"CUST-{vendor.vendor_code}-{i:04d}" + + # Check if customer already exists + existing_customer = db.query(Customer).filter( + Customer.vendor_id == vendor.id, + Customer.email == email + ).first() + + if existing_customer: + customers.append(existing_customer) + continue # Skip creation, customer already exists + + customer = Customer( + vendor_id=vendor.id, + email=email, + hashed_password=auth_manager.hash_password(demo_password), + first_name=f"Customer{i}", + last_name=f"Test", + phone=f"+352123456{i:03d}", + customer_number=customer_number, + is_active=True, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.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 {vendor.name}") + else: + print_warning(f"Customers already exist for {vendor.name}") + + return customers + + +def create_demo_products(db: Session, vendor: Vendor, count: int) -> List[Product]: + """Create demo products for a vendor.""" + + products = [] + + for i in range(1, count + 1): + marketplace_product_id = f"{vendor.vendor_code}-MP-{i:04d}" + product_id = f"{vendor.vendor_code}-PROD-{i:03d}" + + # Check if this product already exists + existing_product = db.query(Product).filter( + Product.vendor_id == vendor.id, + Product.product_id == 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, + title=f"Sample Product {i} - {vendor.name}", + description=f"This is a demo product for testing purposes in {vendor.name}. High quality and affordable.", + link=f"https://{vendor.subdomain}.example.com/products/sample-{i}", + image_link=f"https://{vendor.subdomain}.example.com/images/product-{i}.jpg", + price=str(Decimal(f"{(i * 10) % 500 + 9.99}")), # Store as string + brand=vendor.name, + gtin=f"TEST{vendor.id:02d}{i:010d}", + availability="in stock", + condition="new", + google_product_category="Electronics > Computers > Laptops", + marketplace="Wizamart", + vendor_name=vendor.name, + currency="EUR", + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc), + ) + db.add(marketplace_product) + db.flush() # Flush to get the marketplace_product.id + + # Create the Product (vendor-specific entry) + product = Product( + vendor_id=vendor.id, + marketplace_product_id=marketplace_product.id, + product_id=product_id, + price=float(Decimal(f"{(i * 10) % 500 + 9.99}")), # Store as float + availability="in stock", + condition="new", + currency="EUR", + is_active=True, + is_featured=(i % 5 == 0), # Every 5th product is featured + display_order=i, + min_quantity=1, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.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 {vendor.name}") + else: + print_warning(f"Products already exist for {vendor.name}") + + return products + + +# ============================================================================= +# 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 vendors + print_step(4, "Creating demo vendors...") + vendors = create_demo_vendors(db, auth_manager) + + # Step 5: Create customers + print_step(5, "Creating demo customers...") + for vendor in vendors: + create_demo_customers( + db, + vendor, + auth_manager, + count=settings.seed_customers_per_vendor + ) + + # Step 6: Create products + print_step(6, "Creating demo products...") + for vendor in vendors: + create_demo_products( + db, + vendor, + count=settings.seed_products_per_vendor + ) + + # 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 + vendor_count = db.query(Vendor).count() + user_count = db.query(User).count() + customer_count = db.query(Customer).count() + product_count = db.query(Product).count() + + print(f"\n📊 Database Status:") + print(f" Vendors: {vendor_count}") + print(f" Users: {user_count}") + print(f" Customers: {customer_count}") + print(f" Products: {product_count}") + + # Show vendor details + vendors = db.query(Vendor).all() + print(f"\n🏪 Demo Vendors:") + for vendor in vendors: + print(f"\n {vendor.name} ({vendor.vendor_code})") + print(f" Subdomain: {vendor.subdomain}.{settings.platform_domain}") + + # Query custom domains separately + custom_domain = db.query(VendorDomain).filter( + VendorDomain.vendor_id == vendor.id, + VendorDomain.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 vendor.is_active else '✗ Inactive'}") + + print(f"\n🔐 Demo Vendor Credentials:") + print("─" * 70) + for i, vendor_data in enumerate(DEMO_VENDOR_USERS[:vendor_count], 1): + vendor = vendors[i - 1] if i <= len(vendors) else None + print(f" Vendor {i}:") + print(f" Username: {vendor_data['username']}") + print(f" Email: {vendor_data['email']}") + print(f" Password: {vendor_data['password']}") + if vendor: + print(f" Login: http://localhost:8000/vendor/{vendor.vendor_code}/login") + print(f" or http://{vendor.subdomain}.localhost:8000/vendor/login") + print() + + print(f"\n🛒 Demo Customer Credentials:") + print("─" * 70) + print(f" All customers:") + print(f" Email: customer1@{{subdomain}}.example.com") + print(f" Password: customer123") + print(f" (Replace {{subdomain}} with vendor subdomain, e.g., wizamart)") + print() + + print(f"\n🏪 Shop Access (Development):") + print("─" * 70) + for vendor in vendors: + print(f" {vendor.name}:") + print(f" Path-based: http://localhost:8000/vendors/{vendor.vendor_code}/shop/") + print(f" Subdomain: http://{vendor.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 vendor:") + print(" • Path-based: http://localhost:8000/vendor/WIZAMART/login") + print(" • Subdomain: http://wizamart.localhost:8000/vendor/login") + print(f" 3. Visit vendor shop: http://localhost:8000/vendors/WIZAMART/shop/") + print(f" 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() diff --git a/scripts/show-frontend-structure.bat b/scripts/show-frontend-structure.bat deleted file mode 100644 index 75bc5107..00000000 --- a/scripts/show-frontend-structure.bat +++ /dev/null @@ -1,66 +0,0 @@ -@echo off -setlocal enabledelayedexpansion -echo Generating frontend structure with statistics... -echo. - -set OUTPUT=frontend-structure.txt - -echo Frontend Folder Structure > %OUTPUT% -echo Generated: %date% %time% >> %OUTPUT% -echo ============================================================================== >> %OUTPUT% -echo. >> %OUTPUT% - -echo. >> %OUTPUT% -echo ╔══════════════════════════════════════════════════════════════════╗ >> %OUTPUT% -echo ║ JINJA2 TEMPLATES ║ >> %OUTPUT% -echo ║ Location: app/templates ║ >> %OUTPUT% -echo ╚══════════════════════════════════════════════════════════════════╝ >> %OUTPUT% -echo. >> %OUTPUT% - -tree /F /A app\templates >> %OUTPUT% - -echo. >> %OUTPUT% -echo. >> %OUTPUT% -echo ╔══════════════════════════════════════════════════════════════════╗ >> %OUTPUT% -echo ║ STATIC ASSETS ║ >> %OUTPUT% -echo ║ Location: static ║ >> %OUTPUT% -echo ╚══════════════════════════════════════════════════════════════════╝ >> %OUTPUT% -echo. >> %OUTPUT% - -tree /F /A static >> %OUTPUT% - -echo. >> %OUTPUT% -echo. >> %OUTPUT% -echo ╔══════════════════════════════════════════════════════════════════╗ >> %OUTPUT% -echo ║ STATISTICS ║ >> %OUTPUT% -echo ╚══════════════════════════════════════════════════════════════════╝ >> %OUTPUT% -echo. >> %OUTPUT% - -echo Templates: >> %OUTPUT% -echo - Total HTML files: >> %OUTPUT% -dir /S /B app\templates\*.html 2>nul | find /C ".html" >> %OUTPUT% -echo - Total Jinja2 files: >> %OUTPUT% -dir /S /B app\templates\*.j2 2>nul | find /C ".j2" >> %OUTPUT% - -echo. >> %OUTPUT% -echo Static Assets: >> %OUTPUT% -echo - JavaScript files: >> %OUTPUT% -dir /S /B static\*.js 2>nul | find /C ".js" >> %OUTPUT% -echo - CSS files: >> %OUTPUT% -dir /S /B static\*.css 2>nul | find /C ".css" >> %OUTPUT% -echo - Image files: >> %OUTPUT% -for %%e in (png jpg jpeg gif svg webp ico) do ( - dir /S /B static\*.%%e 2>nul | find /C ".%%e" >> %OUTPUT% -) - -echo. >> %OUTPUT% -echo ============================================================================== >> %OUTPUT% -echo End of structure >> %OUTPUT% - -echo. -echo ✅ Structure saved to %OUTPUT% -echo. -echo Opening file... -notepad %OUTPUT% - -endlocal \ No newline at end of file diff --git a/scripts/show_structure.py b/scripts/show_structure.py new file mode 100644 index 00000000..6d3eeb43 --- /dev/null +++ b/scripts/show_structure.py @@ -0,0 +1,528 @@ +#!/usr/bin/env python3 +""" +Cross-platform structure generator for Wizamart project. +Works on Windows, Linux, and macOS. + +Usage: + python show_structure.py frontend + python show_structure.py backend + python show_structure.py tests + python show_structure.py all +""" + +import os +import sys +import subprocess +from datetime import datetime +from pathlib import Path +from typing import List, Dict + + +def count_files(directory: str, pattern: str) -> int: + """Count files matching pattern in directory.""" + if not os.path.exists(directory): + return 0 + + count = 0 + for root, dirs, files in os.walk(directory): + # Skip __pycache__ and other cache directories + dirs[:] = [d for d in dirs if d not in ['__pycache__', '.pytest_cache', '.git', 'node_modules']] + + for file in files: + if pattern == '*' or file.endswith(pattern): + count += 1 + return count + + +def get_tree_structure(directory: str, exclude_patterns: List[str] = None) -> str: + """Generate tree structure for directory.""" + if not os.path.exists(directory): + return f"Directory {directory} not found" + + # Try to use system tree command first + try: + if sys.platform == 'win32': + # Windows tree command + result = subprocess.run( + ['tree', '/F', '/A', directory], + capture_output=True, + text=True, + encoding='utf-8', + errors='replace' + ) + return result.stdout + else: + # Linux/Mac tree command with exclusions + exclude_args = [] + if exclude_patterns: + exclude_args = ['-I', '|'.join(exclude_patterns)] + + result = subprocess.run( + ['tree', '-F', '-a'] + exclude_args + [directory], + capture_output=True, + text=True + ) + if result.returncode == 0: + return result.stdout + except (subprocess.SubprocessError, FileNotFoundError): + pass + + # Fallback: generate tree structure manually + return generate_manual_tree(directory, exclude_patterns) + + +def generate_manual_tree(directory: str, exclude_patterns: List[str] = None, prefix: str = "") -> str: + """Generate tree structure manually when tree command is not available.""" + if exclude_patterns is None: + exclude_patterns = ['__pycache__', '.pytest_cache', '.git', 'node_modules', '*.pyc', '*.pyo'] + + output = [] + path = Path(directory) + + try: + items = sorted(path.iterdir(), key=lambda x: (not x.is_dir(), x.name)) + + for i, item in enumerate(items): + # Skip excluded patterns + skip = False + for pattern in exclude_patterns: + if pattern.startswith('*'): + # File extension pattern + if item.name.endswith(pattern[1:]): + skip = True + break + else: + # Directory or exact name pattern + if item.name == pattern or pattern in str(item): + skip = True + break + + if skip: + continue + + is_last = i == len(items) - 1 + current_prefix = "└── " if is_last else "├── " + + if item.is_dir(): + output.append(f"{prefix}{current_prefix}{item.name}/") + extension = " " if is_last else "│ " + subtree = generate_manual_tree(str(item), exclude_patterns, prefix + extension) + if subtree: + output.append(subtree) + else: + output.append(f"{prefix}{current_prefix}{item.name}") + except PermissionError: + output.append(f"{prefix}[Permission Denied]") + + return "\n".join(output) + + +def generate_frontend_structure() -> str: + """Generate frontend structure report.""" + output = [] + output.append("Frontend Folder Structure") + output.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + output.append("=" * 78) + output.append("") + + # Templates section + output.append("") + output.append("╔══════════════════════════════════════════════════════════════════╗") + output.append("║ JINJA2 TEMPLATES ║") + output.append("║ Location: app/templates ║") + output.append("╚══════════════════════════════════════════════════════════════════╝") + output.append("") + output.append(get_tree_structure("app/templates")) + + # Static assets section + output.append("") + output.append("") + output.append("╔══════════════════════════════════════════════════════════════════╗") + output.append("║ STATIC ASSETS ║") + output.append("║ Location: static ║") + output.append("╚══════════════════════════════════════════════════════════════════╝") + output.append("") + output.append(get_tree_structure("static")) + + # Documentation section (if exists) + if os.path.exists("docs"): + output.append("") + output.append("") + output.append("╔══════════════════════════════════════════════════════════════════╗") + output.append("║ DOCUMENTATION ║") + output.append("║ Location: docs ║") + output.append("║ (also listed in tools structure) ║") + output.append("╚══════════════════════════════════════════════════════════════════╝") + output.append("") + output.append("Note: Documentation is also included in tools structure") + output.append(" for infrastructure/DevOps context.") + + # Statistics section + output.append("") + output.append("") + output.append("╔══════════════════════════════════════════════════════════════════╗") + output.append("║ STATISTICS ║") + output.append("╚══════════════════════════════════════════════════════════════════╝") + output.append("") + + output.append("Templates:") + output.append(f" - Total HTML files: {count_files('app/templates', '.html')}") + output.append(f" - Total Jinja2 files: {count_files('app/templates', '.j2')}") + + output.append("") + output.append("Static Assets:") + output.append(f" - JavaScript files: {count_files('static', '.js')}") + output.append(f" - CSS files: {count_files('static', '.css')}") + output.append(" - Image files:") + for ext in ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico']: + count = count_files('static', f'.{ext}') + if count > 0: + output.append(f" - .{ext}: {count}") + + if os.path.exists("docs"): + output.append("") + output.append("Documentation:") + output.append(f" - Markdown files: {count_files('docs', '.md')}") + output.append(f" - reStructuredText files: {count_files('docs', '.rst')}") + + output.append("") + output.append("=" * 78) + output.append("End of structure") + + return "\n".join(output) + + +def generate_backend_structure() -> str: + """Generate backend structure report.""" + output = [] + output.append("Backend Folder Structure") + output.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + output.append("=" * 78) + output.append("") + + exclude = ['__pycache__', '*.pyc', '*.pyo', '.pytest_cache', '*.egg-info', 'templates'] + + # Backend directories to include + backend_dirs = [ + ('app', 'Application Code'), + ('middleware', 'Middleware Components'), + ('models', 'Database Models'), + ('storage', 'File Storage'), + ('tasks', 'Background Tasks'), + ('logs', 'Application Logs'), + ] + + for directory, title in backend_dirs: + if os.path.exists(directory): + output.append("") + output.append("╔══════════════════════════════════════════════════════════════════╗") + output.append(f"║ {title.upper().center(62)} ║") + output.append(f"║ Location: {directory + '/'.ljust(51)} ║") + output.append("╚══════════════════════════════════════════════════════════════════╝") + output.append("") + output.append(get_tree_structure(directory, exclude)) + + # Statistics section + output.append("") + output.append("") + output.append("╔══════════════════════════════════════════════════════════════════╗") + output.append("║ STATISTICS ║") + output.append("╚══════════════════════════════════════════════════════════════════╝") + output.append("") + + output.append("Python Files by Directory:") + total_py_files = 0 + for directory, title in backend_dirs: + if os.path.exists(directory): + count = count_files(directory, '.py') + total_py_files += count + output.append(f" - {directory}/: {count} files") + + output.append(f" - Total Python files: {total_py_files}") + + output.append("") + output.append("Application Components (if in app/):") + components = ['routes', 'services', 'schemas', 'exceptions', 'utils'] + for component in components: + component_path = f"app/{component}" + if os.path.exists(component_path): + count = count_files(component_path, '.py') + output.append(f" - app/{component}: {count} files") + + output.append("") + output.append("=" * 78) + output.append("End of structure") + + return "\n".join(output) + + +def generate_tools_structure() -> str: + """Generate tools/infrastructure structure report.""" + output = [] + output.append("Tools & Infrastructure Structure") + output.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + output.append("=" * 78) + output.append("") + + exclude = ['__pycache__', '*.pyc', '*.pyo', '.pytest_cache', '*.egg-info'] + + # Tools directories to include + tools_dirs = [ + ('alembic', 'Database Migrations'), + ('scripts', 'Utility Scripts'), + ('docker', 'Docker Configuration'), + ('docs', 'Documentation'), + ] + + for directory, title in tools_dirs: + if os.path.exists(directory): + output.append("") + output.append("╔══════════════════════════════════════════════════════════════════╗") + output.append(f"║ {title.upper().center(62)} ║") + output.append(f"║ Location: {directory + '/'.ljust(51)} ║") + output.append("╚══════════════════════════════════════════════════════════════════╝") + output.append("") + output.append(get_tree_structure(directory, exclude)) + + # Configuration files section + output.append("") + output.append("") + output.append("╔══════════════════════════════════════════════════════════════════╗") + output.append("║ CONFIGURATION FILES ║") + output.append("╚══════════════════════════════════════════════════════════════════╝") + output.append("") + + output.append("Root configuration files:") + config_files = [ + ('Makefile', 'Build automation'), + ('requirements.txt', 'Python dependencies'), + ('pyproject.toml', 'Python project config'), + ('setup.py', 'Python setup script'), + ('setup.cfg', 'Setup configuration'), + ('alembic.ini', 'Alembic migrations config'), + ('mkdocs.yml', 'MkDocs documentation config'), + ('Dockerfile', 'Docker image definition'), + ('docker-compose.yml', 'Docker services'), + ('.dockerignore', 'Docker ignore patterns'), + ('.gitignore', 'Git ignore patterns'), + ('.env.example', 'Environment variables template'), + ] + + for file, description in config_files: + if os.path.exists(file): + output.append(f" ✓ {file.ljust(25)} - {description}") + + # Statistics section + output.append("") + output.append("") + output.append("╔══════════════════════════════════════════════════════════════════╗") + output.append("║ STATISTICS ║") + output.append("╚══════════════════════════════════════════════════════════════════╝") + output.append("") + + output.append("Database Migrations:") + if os.path.exists("alembic/versions"): + migration_count = count_files("alembic/versions", ".py") + output.append(f" - Total migrations: {migration_count}") + if migration_count > 0: + # Get first and last migration + try: + migrations = sorted([f for f in os.listdir("alembic/versions") if f.endswith('.py')]) + if migrations: + output.append(f" - First: {migrations[0][:40]}...") + if len(migrations) > 1: + output.append(f" - Latest: {migrations[-1][:40]}...") + except Exception: + pass + else: + output.append(" - No alembic/versions directory found") + + output.append("") + output.append("Scripts:") + if os.path.exists("scripts"): + script_types = { + '.py': 'Python scripts', + '.sh': 'Shell scripts', + '.bat': 'Batch scripts', + } + for ext, desc in script_types.items(): + count = count_files("scripts", ext) + if count > 0: + output.append(f" - {desc}: {count}") + else: + output.append(" - No scripts directory found") + + output.append("") + output.append("Documentation:") + if os.path.exists("docs"): + doc_types = { + '.md': 'Markdown files', + '.rst': 'reStructuredText files', + } + for ext, desc in doc_types.items(): + count = count_files("docs", ext) + if count > 0: + output.append(f" - {desc}: {count}") + else: + output.append(" - No docs directory found") + + output.append("") + output.append("Docker:") + docker_files = ['Dockerfile', 'docker-compose.yml', '.dockerignore'] + docker_exists = any(os.path.exists(f) for f in docker_files) + if docker_exists: + output.append(" ✓ Docker configuration present") + if os.path.exists("docker"): + output.append(f" - Docker directory files: {count_files('docker', '*')}") + else: + output.append(" - No Docker configuration found") + + output.append("") + output.append("=" * 78) + output.append("End of structure") + + return "\n".join(output) + + +def generate_test_structure() -> str: + """Generate test structure report.""" + output = [] + output.append("Test Folder Structure") + output.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + output.append("=" * 78) + output.append("") + + # Test files section + output.append("") + output.append("╔══════════════════════════════════════════════════════════════════╗") + output.append("║ TEST FILES ║") + output.append("║ Location: tests/ ║") + output.append("╚══════════════════════════════════════════════════════════════════╝") + output.append("") + + exclude = ['__pycache__', '*.pyc', '*.pyo', '.pytest_cache', '*.egg-info'] + output.append(get_tree_structure("tests", exclude)) + + # Configuration section + output.append("") + output.append("") + output.append("╔══════════════════════════════════════════════════════════════════╗") + output.append("║ TEST CONFIGURATION ║") + output.append("╚══════════════════════════════════════════════════════════════════╝") + output.append("") + + output.append("Test configuration files:") + test_config_files = ['pytest.ini', 'conftest.py', 'tests/conftest.py', '.coveragerc'] + for file in test_config_files: + if os.path.exists(file): + output.append(f" ✓ {file}") + + # Statistics section + output.append("") + output.append("") + output.append("╔══════════════════════════════════════════════════════════════════╗") + output.append("║ STATISTICS ║") + output.append("╚══════════════════════════════════════════════════════════════════╝") + output.append("") + + # Count test files + test_file_count = 0 + if os.path.exists("tests"): + for root, dirs, files in os.walk("tests"): + dirs[:] = [d for d in dirs if d != '__pycache__'] + for file in files: + if file.startswith("test_") and file.endswith(".py"): + test_file_count += 1 + + output.append("Test Files:") + output.append(f" - Total test files: {test_file_count}") + + output.append("") + output.append("By Category:") + categories = ['unit', 'integration', 'system', 'e2e', 'performance'] + for category in categories: + category_path = f"tests/{category}" + if os.path.exists(category_path): + count = 0 + for root, dirs, files in os.walk(category_path): + dirs[:] = [d for d in dirs if d != '__pycache__'] + for file in files: + if file.startswith("test_") and file.endswith(".py"): + count += 1 + output.append(f" - tests/{category}: {count} files") + + # Count test functions + test_function_count = 0 + if os.path.exists("tests"): + for root, dirs, files in os.walk("tests"): + dirs[:] = [d for d in dirs if d != '__pycache__'] + for file in files: + if file.startswith("test_") and file.endswith(".py"): + filepath = os.path.join(root, file) + try: + with open(filepath, 'r', encoding='utf-8') as f: + for line in f: + if line.strip().startswith("def test_"): + test_function_count += 1 + except Exception: + pass + + output.append("") + output.append("Test Functions:") + output.append(f" - Total test functions: {test_function_count}") + + output.append("") + output.append("Coverage Files:") + if os.path.exists(".coverage"): + output.append(" ✓ .coverage (coverage data file exists)") + if os.path.exists("htmlcov"): + output.append(" ✓ htmlcov/ (HTML coverage report exists)") + + output.append("") + output.append("=" * 78) + output.append("End of structure") + + return "\n".join(output) + + +def main(): + """Main entry point.""" + if len(sys.argv) < 2: + print("Usage: python show_structure.py [frontend|backend|tests|tools|all]") + sys.exit(1) + + structure_type = sys.argv[1].lower() + + generators = { + 'frontend': ('frontend-structure.txt', generate_frontend_structure), + 'backend': ('backend-structure.txt', generate_backend_structure), + 'tests': ('test-structure.txt', generate_test_structure), + 'tools': ('tools-structure.txt', generate_tools_structure), + } + + if structure_type == 'all': + for name, (filename, generator) in generators.items(): + print(f"\n{'=' * 60}") + print(f"Generating {name} structure...") + print('=' * 60) + content = generator() + with open(filename, 'w', encoding='utf-8') as f: + f.write(content) + print(f"✅ {name.capitalize()} structure saved to {filename}") + print(f"\n{content}\n") + elif structure_type in generators: + filename, generator = generators[structure_type] + print(f"Generating {structure_type} structure...") + content = generator() + with open(filename, 'w', encoding='utf-8') as f: + f.write(content) + print(f"\n✅ Structure saved to {filename}\n") + print(content) + else: + print(f"Error: Unknown structure type '{structure_type}'") + print("Valid options: frontend, backend, tests, tools, all") + sys.exit(1) + + +if __name__ == "__main__": + main()