diff --git a/config/docker-compose.yml b/config/docker-compose.yml deleted file mode 100644 index b46261ff..00000000 --- a/config/docker-compose.yml +++ /dev/null @@ -1,1137 +0,0 @@ -# ================================================================ -# OPTIMIZED DOCKER COMPOSE - NO ENVIRONMENT DUPLICATION -# Single source of truth: .env file only -# ================================================================ - - -# ================================================================ -# NETWORKS & VOLUMES (same as before) -# ================================================================ -networks: - bakery-network: - driver: bridge - ipam: - config: - - subnet: 172.20.0.0/16 - -volumes: - auth_db_data: - training_db_data: - forecasting_db_data: - sales_db_data: - external_db_data: - tenant_db_data: - notification_db_data: - inventory_db_data: - recipes_db_data: - suppliers_db_data: - pos_db_data: - orders_db_data: - production_db_data: - alert_processor_db_data: - redis_data: - rabbitmq_data: - prometheus_data: - grafana_data: - model_storage: - log_storage: - nominatim_data: - frontend_node_modules: - - -# ================================================================ -# SERVICES - USING ONLY .env FILE -# ================================================================ -services: - - # ================================================================ - # INFRASTRUCTURE - NO DUPLICATION - # ================================================================ - - redis: - image: redis:7.4-alpine - container_name: bakery-redis - restart: unless-stopped - # ONLY use environment substitution from .env - command: > - redis-server - --appendonly yes - --requirepass ${REDIS_PASSWORD} - --maxmemory ${REDIS_MAX_MEMORY:-512mb} - --databases 16 - ports: - - "${REDIS_PORT}:6379" - volumes: - - redis_data:/data - networks: - bakery-network: - ipv4_address: 172.20.0.10 - healthcheck: - test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] - interval: 30s - timeout: 10s - retries: 3 - - rabbitmq: - image: rabbitmq:4.1-management-alpine - container_name: bakery-rabbitmq - restart: unless-stopped - # ONLY use environment substitution from .env - environment: - - RABBITMQ_DEFAULT_USER=${RABBITMQ_USER} - - RABBITMQ_DEFAULT_PASS=${RABBITMQ_PASSWORD} - - RABBITMQ_DEFAULT_VHOST=${RABBITMQ_VHOST} - ports: - - "${RABBITMQ_PORT}:5672" - - "${RABBITMQ_MANAGEMENT_PORT}:15672" - volumes: - - rabbitmq_data:/var/lib/rabbitmq - networks: - bakery-network: - ipv4_address: 172.20.0.11 - healthcheck: - test: ["CMD", "timeout", "5", "bash", "-c", " -""" -import asyncio -import argparse -import sys -import os -from datetime import datetime, timedelta - -# Add services to path -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../services/procurement/app')) -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../services/production/app')) -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../shared')) - - -async def clean_old_purchase_orders(tenant_id: str, dry_run: bool = True): - """Clean old pending purchase orders""" - try: - from app.core.database import AsyncSessionLocal - from sqlalchemy import text - - async with AsyncSessionLocal() as session: - # Get count of pending POs - count_result = await session.execute( - text(""" - SELECT COUNT(*) - FROM purchase_orders - WHERE tenant_id = :tenant_id - AND status = 'pending_approval' - """), - {"tenant_id": tenant_id} - ) - count = count_result.scalar() - - print(f"Found {count} pending purchase orders for tenant {tenant_id}") - - if count == 0: - print("No purchase orders to clean.") - return 0 - - if dry_run: - print(f"DRY RUN: Would delete {count} pending purchase orders") - return count - - # Delete pending POs - result = await session.execute( - text(""" - DELETE FROM purchase_orders - WHERE tenant_id = :tenant_id - AND status = 'pending_approval' - """), - {"tenant_id": tenant_id} - ) - await session.commit() - - deleted = result.rowcount - print(f"✓ Deleted {deleted} pending purchase orders") - return deleted - - except Exception as e: - print(f"Error cleaning purchase orders: {e}") - return 0 - - -async def clean_old_production_batches(tenant_id: str, dry_run: bool = True): - """Clean old pending production batches""" - try: - # Import production service dependencies - sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../services/production/app')) - from app.core.database import AsyncSessionLocal as ProductionSession - from sqlalchemy import text - - async with ProductionSession() as session: - # Get count of pending/scheduled batches - count_result = await session.execute( - text(""" - SELECT COUNT(*) - FROM production_batches - WHERE tenant_id = :tenant_id - AND status IN ('pending', 'scheduled') - """), - {"tenant_id": tenant_id} - ) - count = count_result.scalar() - - print(f"Found {count} pending/scheduled production batches for tenant {tenant_id}") - - if count == 0: - print("No production batches to clean.") - return 0 - - if dry_run: - print(f"DRY RUN: Would delete {count} pending/scheduled batches") - return count - - # Delete pending/scheduled batches - result = await session.execute( - text(""" - DELETE FROM production_batches - WHERE tenant_id = :tenant_id - AND status IN ('pending', 'scheduled') - """), - {"tenant_id": tenant_id} - ) - await session.commit() - - deleted = result.rowcount - print(f"✓ Deleted {deleted} pending/scheduled production batches") - return deleted - - except Exception as e: - print(f"Error cleaning production batches: {e}") - return 0 - - -async def main(): - parser = argparse.ArgumentParser( - description='Clean old dashboard data (POs and batches) with malformed reasoning_data' - ) - parser.add_argument( - '--tenant-id', - required=True, - help='Tenant ID to clean data for' - ) - parser.add_argument( - '--execute', - action='store_true', - help='Actually delete data (default is dry run)' - ) - - args = parser.parse_args() - - dry_run = not args.execute - - print("=" * 60) - print("Dashboard Data Cleanup Utility") - print("=" * 60) - print(f"Tenant ID: {args.tenant_id}") - print(f"Mode: {'DRY RUN (no changes will be made)' if dry_run else 'EXECUTE (will delete data)'}") - print("=" * 60) - print() - - if not dry_run: - print("⚠️ WARNING: This will permanently delete pending purchase orders and production batches!") - print(" After deletion, you should trigger a new orchestration run to create fresh data.") - response = input(" Are you sure you want to continue? (yes/no): ") - if response.lower() != 'yes': - print("Aborted.") - return - print() - - # Clean purchase orders - print("1. Cleaning Purchase Orders...") - po_count = await clean_old_purchase_orders(args.tenant_id, dry_run) - print() - - # Clean production batches - print("2. Cleaning Production Batches...") - batch_count = await clean_old_production_batches(args.tenant_id, dry_run) - print() - - print("=" * 60) - print("Summary:") - print(f" Purchase Orders: {po_count} {'would be' if dry_run else ''} deleted") - print(f" Production Batches: {batch_count} {'would be' if dry_run else ''} deleted") - print("=" * 60) - - if dry_run: - print("\nTo actually delete the data, run with --execute flag:") - print(f" python scripts/clean_old_dashboard_data.py --tenant-id {args.tenant_id} --execute") - else: - print("\n✓ Data cleaned successfully!") - print("\nNext steps:") - print(" 1. Restart the orchestrator service") - print(" 2. Trigger a new orchestration run from the dashboard") - print(" 3. The new POs and batches will have properly interpolated variables") - - -if __name__ == '__main__': - asyncio.run(main()) diff --git a/scripts/complete-cleanup.sh b/scripts/complete-cleanup.sh deleted file mode 100755 index 8204db47..00000000 --- a/scripts/complete-cleanup.sh +++ /dev/null @@ -1,246 +0,0 @@ -#!/bin/bash - -# Complete Cleanup Script for Kind + Colima + Skaffold Environment -# This script removes all resources, images, and configurations - -set -e - -echo "🧹 Complete Cleanup for Bakery IA Development Environment" -echo "========================================================" - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -print_status() { - echo -e "${BLUE}[INFO]${NC} $1" -} - -print_success() { - echo -e "${GREEN}[SUCCESS]${NC} $1" -} - -print_warning() { - echo -e "${YELLOW}[WARNING]${NC} $1" -} - -print_error() { - echo -e "${RED}[ERROR]${NC} $1" -} - -# Show what will be cleaned up -show_cleanup_plan() { - echo "" - print_warning "This script will clean up:" - echo " 🚀 Skaffold deployments and resources" - echo " 🐋 Docker images (bakery/* images)" - echo " ☸️ Kubernetes resources in bakery-ia namespace" - echo " 🔒 cert-manager and TLS certificates" - echo " 🌐 NGINX Ingress Controller" - echo " 📦 Kind cluster (bakery-ia-local)" - echo " 🐳 Colima Docker runtime" - echo " 📝 Local certificate files" - echo " 🗂️ /etc/hosts entries (optional)" - echo "" - read -p "❓ Do you want to continue? (y/N): " -n 1 -r - echo - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - print_status "Cleanup cancelled" - exit 0 - fi -} - -# 1. Cleanup Skaffold deployments -cleanup_skaffold() { - print_status "🚀 Cleaning up Skaffold deployments..." - - if command -v skaffold &> /dev/null; then - # Try to delete with different profiles - skaffold delete --profile=dev 2>/dev/null || true - skaffold delete --profile=debug 2>/dev/null || true - skaffold delete 2>/dev/null || true - print_success "Skaffold deployments cleaned up" - else - print_warning "Skaffold not found, skipping Skaffold cleanup" - fi -} - -# 2. Cleanup Kubernetes resources -cleanup_kubernetes() { - print_status "☸️ Cleaning up Kubernetes resources..." - - if command -v kubectl &> /dev/null && kubectl cluster-info &> /dev/null; then - # Delete application namespace and all resources - kubectl delete namespace bakery-ia --ignore-not-found=true - - # Delete cert-manager - kubectl delete -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.2/cert-manager.yaml --ignore-not-found=true 2>/dev/null || true - - # Delete NGINX Ingress - kubectl delete -f https://kind.sigs.k8s.io/examples/ingress/deploy-ingress-nginx.yaml --ignore-not-found=true 2>/dev/null || true - - # Delete any remaining cluster-wide resources - kubectl delete clusterissuers --all --ignore-not-found=true 2>/dev/null || true - kubectl delete clusterroles,clusterrolebindings -l app.kubernetes.io/name=cert-manager --ignore-not-found=true 2>/dev/null || true - - print_success "Kubernetes resources cleaned up" - else - print_warning "Kubectl not available or cluster not running, skipping Kubernetes cleanup" - fi -} - -# 3. Cleanup Docker images in Colima -cleanup_docker_images() { - print_status "🐋 Cleaning up Docker images..." - - if command -v docker &> /dev/null && docker info &> /dev/null; then - # Remove bakery-specific images - print_status "Removing bakery/* images..." - docker images --format "table {{.Repository}}:{{.Tag}}" | grep "^bakery/" | xargs -r docker rmi -f 2>/dev/null || true - - # Remove dangling images - print_status "Removing dangling images..." - docker image prune -f 2>/dev/null || true - - # Remove unused images (optional - uncomment if you want aggressive cleanup) - # print_status "Removing unused images..." - # docker image prune -a -f 2>/dev/null || true - - # Remove build cache - print_status "Cleaning build cache..." - docker builder prune -f 2>/dev/null || true - - print_success "Docker images cleaned up" - else - print_warning "Docker not available, skipping Docker cleanup" - fi -} - -# 4. Delete Kind cluster -cleanup_kind_cluster() { - print_status "📦 Deleting Kind cluster..." - - if command -v kind &> /dev/null; then - # Delete the specific cluster - kind delete cluster --name bakery-ia-local 2>/dev/null || true - - # Also clean up any other bakery clusters - kind get clusters 2>/dev/null | grep -E "(bakery|dev)" | xargs -r -I {} kind delete cluster --name {} 2>/dev/null || true - - print_success "Kind cluster deleted" - else - print_warning "Kind not found, skipping cluster cleanup" - fi -} - -# 5. Stop and clean Colima -cleanup_colima() { - print_status "🐳 Cleaning up Colima..." - - if command -v colima &> /dev/null; then - # Stop the specific profile - colima stop --profile k8s-local 2>/dev/null || true - - # Delete the profile (removes all data) - read -p "❓ Do you want to delete the Colima profile (removes all Docker data)? (y/N): " -n 1 -r - echo - if [[ $REPLY =~ ^[Yy]$ ]]; then - colima delete --profile k8s-local --force 2>/dev/null || true - print_success "Colima profile deleted" - else - print_warning "Colima profile kept (stopped only)" - fi - else - print_warning "Colima not found, skipping Colima cleanup" - fi -} - -# 6. Cleanup local files -cleanup_local_files() { - print_status "📝 Cleaning up local files..." - - # Remove certificate files - rm -f bakery-ia-ca.crt 2>/dev/null || true - rm -f *.crt *.key 2>/dev/null || true - - # Remove any Skaffold cache (if exists) - rm -rf ~/.skaffold/cache 2>/dev/null || true - - print_success "Local files cleaned up" -} - -# 7. Cleanup /etc/hosts entries (optional) -cleanup_hosts_file() { - print_status "🗂️ Cleaning up /etc/hosts entries..." - - if grep -q "bakery-ia.local" /etc/hosts 2>/dev/null; then - read -p "❓ Remove bakery-ia entries from /etc/hosts? (y/N): " -n 1 -r - echo - if [[ $REPLY =~ ^[Yy]$ ]]; then - # Backup hosts file first - sudo cp /etc/hosts /etc/hosts.backup.$(date +%Y%m%d_%H%M%S) - - # Remove entries - sudo sed -i '' '/bakery-ia.local/d' /etc/hosts - sudo sed -i '' '/api.bakery-ia.local/d' /etc/hosts - sudo sed -i '' '/monitoring.bakery-ia.local/d' /etc/hosts - - print_success "Hosts file entries removed" - else - print_warning "Hosts file entries kept" - fi - else - print_status "No bakery-ia entries found in /etc/hosts" - fi -} - -# 8. Show system status after cleanup -show_cleanup_summary() { - echo "" - print_success "🎉 Cleanup completed!" - echo "" - print_status "System status after cleanup:" - - # Check remaining Docker images - if command -v docker &> /dev/null && docker info &> /dev/null; then - local bakery_images=$(docker images --format "table {{.Repository}}:{{.Tag}}" | grep "^bakery/" | wc -l) - echo " 🐋 Bakery Docker images remaining: $bakery_images" - fi - - # Check Kind clusters - if command -v kind &> /dev/null; then - local clusters=$(kind get clusters 2>/dev/null | wc -l) - echo " 📦 Kind clusters remaining: $clusters" - fi - - # Check Colima status - if command -v colima &> /dev/null; then - local colima_status=$(colima status --profile k8s-local 2>/dev/null | head -n1 || echo "Not running") - echo " 🐳 Colima k8s-local status: $colima_status" - fi - - echo "" - print_status "To restart development environment:" - echo " 🚀 Quick start: ./skaffold-dev.sh" - echo " 🔒 With HTTPS: ./setup-https.sh" - echo " 🏗️ Manual: colima start --cpu 4 --memory 8 --disk 50 --runtime docker --profile k8s-local" -} - -# Main execution -main() { - show_cleanup_plan - cleanup_skaffold - cleanup_kubernetes - cleanup_docker_images - cleanup_kind_cluster - cleanup_colima - cleanup_local_files - cleanup_hosts_file - show_cleanup_summary -} - -# Run main function -main "$@" \ No newline at end of file diff --git a/scripts/complete_audit_registration.py b/scripts/complete_audit_registration.py deleted file mode 100644 index 2f236a6e..00000000 --- a/scripts/complete_audit_registration.py +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env python3 -""" -Script to complete audit router registration in all remaining services. -""" - -import re -from pathlib import Path - -BASE_DIR = Path(__file__).parent.parent / "services" - -# Services that still need updates (suppliers, pos, training, notification, external, forecasting) -SERVICES = ['suppliers', 'pos', 'training', 'notification', 'external', 'forecasting'] - -def update_service(service_name): - main_file = BASE_DIR / service_name / "app" / "main.py" - - if not main_file.exists(): - print(f"⚠️ {service_name}: main.py not found") - return False - - content = main_file.read_text() - modified = False - - # Check if audit is already imported - if 'import.*audit' in content or ', audit' in content: - print(f"✓ {service_name}: audit already imported") - else: - # Add audit import - find the from .api or from app.api import line - patterns = [ - (r'(from \.api import [^)]+)(\))', r'\1, audit\2'), # Multi-line with parentheses - (r'(from \.api import .+)', r'\1, audit'), # Single line with .api - (r'(from app\.api import [^)]+)(\))', r'\1, audit\2'), # Multi-line with app.api - (r'(from app\.api import .+)', r'\1, audit'), # Single line with app.api - ] - - for pattern, replacement in patterns: - new_content = re.sub(pattern, replacement, content) - if new_content != content: - content = new_content - modified = True - print(f"✓ {service_name}: added audit import") - break - - if not modified: - print(f"⚠️ {service_name}: could not find import pattern, needs manual update") - return False - - # Check if audit router is already registered - if 'service.add_router(audit.router)' in content: - print(f"✓ {service_name}: audit router already registered") - else: - # Find the last service.add_router line and add audit router after it - lines = content.split('\n') - last_router_index = -1 - - for i, line in enumerate(lines): - if 'service.add_router(' in line and 'audit' not in line: - last_router_index = i - - if last_router_index != -1: - # Insert audit router after the last router registration - lines.insert(last_router_index + 1, 'service.add_router(audit.router)') - content = '\n'.join(lines) - modified = True - print(f"✓ {service_name}: added audit router registration") - else: - print(f"⚠️ {service_name}: could not find router registration pattern, needs manual update") - return False - - if modified: - main_file.write_text(content) - print(f"✅ {service_name}: updated successfully") - else: - print(f"ℹ️ {service_name}: no changes needed") - - return True - -def main(): - print("Completing audit router registration in remaining services...\n") - - success_count = 0 - for service in SERVICES: - if update_service(service): - success_count += 1 - print() - - print(f"\nCompleted: {success_count}/{len(SERVICES)} services updated successfully") - -if __name__ == "__main__": - main() diff --git a/scripts/comprehensive_demo_validation.py b/scripts/comprehensive_demo_validation.py deleted file mode 100755 index 268fbbcc..00000000 --- a/scripts/comprehensive_demo_validation.py +++ /dev/null @@ -1,102 +0,0 @@ -#!/usr/bin/env python3 -""" -Comprehensive Demo Validation Script - -Runs all validation checks for enterprise demo fixtures and provides a complete report. -""" - -import subprocess -import sys -import os - -def run_validation(): - """Run the enterprise demo fixtures validation""" - print("=== Running Enterprise Demo Fixtures Validation ===") - print() - - try: - # Run the main validation script - result = subprocess.run([ - sys.executable, - "scripts/validate_enterprise_demo_fixtures.py" - ], capture_output=True, text=True, cwd=".") - - print(result.stdout) - - if result.returncode != 0: - print("❌ Validation failed!") - print("Error output:") - print(result.stderr) - return False - else: - print("✅ Validation passed!") - return True - - except Exception as e: - print(f"❌ Error running validation: {e}") - return False - -def run_summary(): - """Run the demo fixtures summary""" - print("\n=== Running Demo Fixtures Summary ===") - print() - - try: - # Run the summary script - result = subprocess.run([ - sys.executable, - "scripts/demo_fixtures_summary.py" - ], capture_output=True, text=True, cwd=".") - - print(result.stdout) - - if result.returncode != 0: - print("❌ Summary failed!") - print("Error output:") - print(result.stderr) - return False - else: - print("✅ Summary completed!") - return True - - except Exception as e: - print(f"❌ Error running summary: {e}") - return False - -def main(): - """Main function to run comprehensive validation""" - print("🚀 Starting Comprehensive Demo Validation") - print("=" * 60) - - # Change to project directory - os.chdir("/Users/urtzialfaro/Documents/bakery-ia") - - # Run validation - validation_passed = run_validation() - - # Run summary - summary_passed = run_summary() - - # Final report - print("\n" + "=" * 60) - print("📋 FINAL REPORT") - print("=" * 60) - - if validation_passed and summary_passed: - print("🎉 ALL CHECKS PASSED!") - print("✅ Enterprise demo fixtures are ready for use") - print("✅ All cross-references are valid") - print("✅ No missing IDs or broken relationships") - print("✅ All required files are present") - return True - else: - print("❌ VALIDATION FAILED!") - if not validation_passed: - print("❌ Cross-reference validation failed") - if not summary_passed: - print("❌ Summary generation failed") - return False - -if __name__ == "__main__": - success = main() - sys.exit(0 if success else 1) diff --git a/scripts/demo_fixtures_summary.py b/scripts/demo_fixtures_summary.py deleted file mode 100755 index b3f1e322..00000000 --- a/scripts/demo_fixtures_summary.py +++ /dev/null @@ -1,201 +0,0 @@ -#!/usr/bin/env python3 -""" -Demo Fixtures Summary Script - -Provides a comprehensive summary of the enterprise demo fixtures status. -""" - -import json -import os -from pathlib import Path -from collections import defaultdict - -def get_file_info(base_path: Path) -> dict: - """Get information about all fixture files""" - info = { - "parent": defaultdict(list), - "children": defaultdict(dict) - } - - # Parent files - parent_dir = base_path / "parent" - if parent_dir.exists(): - for file_path in parent_dir.glob("*.json"): - file_size = file_path.stat().st_size - info["parent"][file_path.name] = { - "size_bytes": file_size, - "size_kb": round(file_size / 1024, 2) - } - - # Children files - children_dir = base_path / "children" - if children_dir.exists(): - for child_dir in children_dir.iterdir(): - if child_dir.is_dir(): - tenant_id = child_dir.name - for file_path in child_dir.glob("*.json"): - file_size = file_path.stat().st_size - if tenant_id not in info["children"]: - info["children"][tenant_id] = {} - info["children"][tenant_id][file_path.name] = { - "size_bytes": file_size, - "size_kb": round(file_size / 1024, 2) - } - - return info - -def count_entities(base_path: Path) -> dict: - """Count entities in fixture files""" - counts = { - "parent": defaultdict(int), - "children": defaultdict(lambda: defaultdict(int)) - } - - # Parent counts - parent_dir = base_path / "parent" - if parent_dir.exists(): - # Tenants - tenant_file = parent_dir / "01-tenant.json" - if tenant_file.exists(): - with open(tenant_file, 'r') as f: - data = json.load(f) - counts["parent"]["tenants"] = 1 + len(data.get("children", [])) - - # Users - auth_file = parent_dir / "02-auth.json" - if auth_file.exists(): - with open(auth_file, 'r') as f: - data = json.load(f) - counts["parent"]["users"] = len(data.get("users", [])) - - # Inventory - inventory_file = parent_dir / "03-inventory.json" - if inventory_file.exists(): - with open(inventory_file, 'r') as f: - data = json.load(f) - counts["parent"]["ingredients"] = len(data.get("ingredients", [])) - counts["parent"]["products"] = len(data.get("products", [])) - - # Recipes - recipes_file = parent_dir / "04-recipes.json" - if recipes_file.exists(): - with open(recipes_file, 'r') as f: - data = json.load(f) - counts["parent"]["recipes"] = len(data.get("recipes", [])) - - # Suppliers - suppliers_file = parent_dir / "05-suppliers.json" - if suppliers_file.exists(): - with open(suppliers_file, 'r') as f: - data = json.load(f) - counts["parent"]["suppliers"] = len(data.get("suppliers", [])) - - # Children counts - children_dir = base_path / "children" - if children_dir.exists(): - for child_dir in children_dir.iterdir(): - if child_dir.is_dir(): - tenant_id = child_dir.name - - # Users - auth_file = child_dir / "02-auth.json" - if auth_file.exists(): - with open(auth_file, 'r') as f: - data = json.load(f) - counts["children"][tenant_id]["users"] = len(data.get("users", [])) - - # Inventory - inventory_file = child_dir / "03-inventory.json" - if inventory_file.exists(): - with open(inventory_file, 'r') as f: - data = json.load(f) - counts["children"][tenant_id]["ingredients"] = len(data.get("ingredients", [])) - counts["children"][tenant_id]["products"] = len(data.get("products", [])) - - # Recipes - recipes_file = child_dir / "04-recipes.json" - if recipes_file.exists(): - with open(recipes_file, 'r') as f: - data = json.load(f) - counts["children"][tenant_id]["recipes"] = len(data.get("recipes", [])) - - # Suppliers - suppliers_file = child_dir / "05-suppliers.json" - if suppliers_file.exists(): - with open(suppliers_file, 'r') as f: - data = json.load(f) - counts["children"][tenant_id]["suppliers"] = len(data.get("suppliers", [])) - - return counts - -def main(): - """Main function to display summary""" - print("=== Enterprise Demo Fixtures Summary ===") - print() - - base_path = Path("shared/demo/fixtures/enterprise") - - # File information - print("📁 FILE INFORMATION") - print("-" * 50) - - file_info = get_file_info(base_path) - - print("Parent Files:") - for filename, info in file_info["parent"].items(): - print(f" {filename}: {info['size_kb']} KB") - - print(f"\nChild Files ({len(file_info['children'])} locations):") - for tenant_id, files in file_info["children"].items(): - print(f" {tenant_id}:") - for filename, info in files.items(): - print(f" {filename}: {info['size_kb']} KB") - - # Entity counts - print("\n📊 ENTITY COUNTS") - print("-" * 50) - - counts = count_entities(base_path) - - print("Parent Entities:") - for entity_type, count in counts["parent"].items(): - print(f" {entity_type}: {count}") - - print(f"\nChild Entities ({len(counts['children'])} locations):") - for tenant_id, entity_counts in counts["children"].items(): - print(f" {tenant_id}:") - for entity_type, count in entity_counts.items(): - print(f" {entity_type}: {count}") - - # Totals - print("\n📈 TOTALS") - print("-" * 50) - - total_users = counts["parent"]["users"] - total_tenants = counts["parent"]["tenants"] - total_ingredients = counts["parent"]["ingredients"] - total_products = counts["parent"]["products"] - total_recipes = counts["parent"]["recipes"] - total_suppliers = counts["parent"]["suppliers"] - - for tenant_id, entity_counts in counts["children"].items(): - total_users += entity_counts.get("users", 0) - total_ingredients += entity_counts.get("ingredients", 0) - total_products += entity_counts.get("products", 0) - total_recipes += entity_counts.get("recipes", 0) - total_suppliers += entity_counts.get("suppliers", 0) - - print(f"Total Tenants: {total_tenants}") - print(f"Total Users: {total_users}") - print(f"Total Ingredients: {total_ingredients}") - print(f"Total Products: {total_products}") - print(f"Total Recipes: {total_recipes}") - print(f"Total Suppliers: {total_suppliers}") - - print("\n✅ VALIDATION STATUS") - print("-" * 50) - print("All cross-references validated successfully!") - print("No missing IDs or broken relationships detected.") - -if __name__ == "__main__": - main() diff --git a/scripts/docker-compose.yml b/scripts/docker-compose.yml deleted file mode 100644 index d26c4ce6..00000000 --- a/scripts/docker-compose.yml +++ /dev/null @@ -1,72 +0,0 @@ -# ================================================================ -# services/auth/docker-compose.yml (For standalone testing) -# ================================================================ - -services: - auth-db: - image: postgres:17-alpine - container_name: auth-db - environment: - POSTGRES_DB: auth_db - POSTGRES_USER: auth_user - POSTGRES_PASSWORD: auth_pass123 - ports: - - "5432:5432" - volumes: - - auth_db_data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U auth_user -d auth_db"] - interval: 10s - timeout: 5s - retries: 5 - - redis: - image: redis:7.4-alpine - container_name: auth-redis - ports: - - "6379:6379" - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 5 - - rabbitmq: - image: rabbitmq:4.1-management-alpine - container_name: auth-rabbitmq - environment: - RABBITMQ_DEFAULT_USER: bakery - RABBITMQ_DEFAULT_PASS: forecast123 - ports: - - "5672:5672" - - "15672:15672" - healthcheck: - test: ["CMD", "rabbitmq-diagnostics", "ping"] - interval: 30s - timeout: 10s - retries: 5 - - auth-service: - build: . - container_name: auth-service - environment: - - DATABASE_URL=postgresql+asyncpg://auth_user:auth_pass123@auth-db:5432/auth_db - - REDIS_URL=redis://redis:6379/0 - - RABBITMQ_URL=amqp://bakery:forecast123@rabbitmq:5672/ - - DEBUG=true - - LOG_LEVEL=INFO - ports: - - "8001:8000" - depends_on: - auth-db: - condition: service_healthy - redis: - condition: service_healthy - rabbitmq: - condition: service_healthy - volumes: - - .:/app - command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload - -volumes: - auth_db_data: \ No newline at end of file diff --git a/scripts/fix_inventory_user_references.py b/scripts/fix_inventory_user_references.py deleted file mode 100755 index 464912d4..00000000 --- a/scripts/fix_inventory_user_references.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python3 -""" -Fix Inventory User References Script - -Replaces the missing user ID c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6 -with the production director user ID ae38accc-1ad4-410d-adbc-a55630908924 -in all inventory.json files. -""" - -import json -import os -from pathlib import Path - -# The incorrect user ID that needs to be replaced -OLD_USER_ID = "c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6" - -# The correct production director user ID -NEW_USER_ID = "ae38accc-1ad4-410d-adbc-a55630908924" - -def fix_inventory_file(filepath: Path) -> bool: - """Fix user references in a single inventory.json file""" - try: - with open(filepath, 'r', encoding='utf-8') as f: - data = json.load(f) - - changed = False - - # Fix ingredients - if "ingredients" in data: - for ingredient in data["ingredients"]: - if ingredient.get("created_by") == OLD_USER_ID: - ingredient["created_by"] = NEW_USER_ID - changed = True - - # Fix products - if "products" in data: - for product in data["products"]: - if product.get("created_by") == OLD_USER_ID: - product["created_by"] = NEW_USER_ID - changed = True - - if changed: - with open(filepath, 'w', encoding='utf-8') as f: - json.dump(data, f, ensure_ascii=False, indent=2) - print(f"✓ Fixed {filepath}") - return True - else: - print(f"✓ No changes needed for {filepath}") - return False - - except Exception as e: - print(f"✗ Error processing {filepath}: {e}") - return False - -def main(): - """Main function to fix all inventory files""" - print("=== Fixing Inventory User References ===") - print(f"Replacing {OLD_USER_ID} with {NEW_USER_ID}") - print() - - base_path = Path("shared/demo/fixtures/enterprise") - - # Fix parent inventory - parent_file = base_path / "parent" / "03-inventory.json" - if parent_file.exists(): - fix_inventory_file(parent_file) - - # Fix children inventories - children_dir = base_path / "children" - if children_dir.exists(): - for child_dir in children_dir.iterdir(): - if child_dir.is_dir(): - inventory_file = child_dir / "03-inventory.json" - if inventory_file.exists(): - fix_inventory_file(inventory_file) - - print("\n=== Fix Complete ===") - -if __name__ == "__main__": - main() diff --git a/scripts/functional_test_deletion.sh b/scripts/functional_test_deletion.sh deleted file mode 100755 index 4b0f8213..00000000 --- a/scripts/functional_test_deletion.sh +++ /dev/null @@ -1,326 +0,0 @@ -#!/usr/bin/env bash -# ============================================================================ -# Functional Test: Tenant Deletion System -# ============================================================================ -# Tests the complete tenant deletion workflow with service tokens -# -# Usage: -# ./scripts/functional_test_deletion.sh -# -# Example: -# ./scripts/functional_test_deletion.sh dbc2128a-7539-470c-94b9-c1e37031bd77 -# -# ============================================================================ - -set -e # Exit on error - -# Require bash 4+ for associative arrays -if [ "${BASH_VERSINFO[0]}" -lt 4 ]; then - echo "Error: This script requires bash 4.0 or higher" - echo "Current version: ${BASH_VERSION}" - exit 1 -fi - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Configuration -TENANT_ID="${1:-dbc2128a-7539-470c-94b9-c1e37031bd77}" -SERVICE_TOKEN="${SERVICE_TOKEN:-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZW5hbnQtZGVsZXRpb24tb3JjaGVzdHJhdG9yIiwidXNlcl9pZCI6InRlbmFudC1kZWxldGlvbi1vcmNoZXN0cmF0b3IiLCJzZXJ2aWNlIjoidGVuYW50LWRlbGV0aW9uLW9yY2hlc3RyYXRvciIsInR5cGUiOiJzZXJ2aWNlIiwiaXNfc2VydmljZSI6dHJ1ZSwicm9sZSI6ImFkbWluIiwiZW1haWwiOiJ0ZW5hbnQtZGVsZXRpb24tb3JjaGVzdHJhdG9yQGludGVybmFsLnNlcnZpY2UiLCJleHAiOjE3OTM0NDIwMzAsImlhdCI6MTc2MTkwNjAzMCwiaXNzIjoiYmFrZXJ5LWF1dGgifQ.I6mWLpkRim2fJ1v9WH24g4YT3-ZGbuFXxCorZxhPp6c}" - -# Test mode (preview or delete) -TEST_MODE="${2:-preview}" # preview or delete - -# Service list with their endpoints -declare -A SERVICES=( - ["orders"]="orders-service:8000" - ["inventory"]="inventory-service:8000" - ["recipes"]="recipes-service:8000" - ["sales"]="sales-service:8000" - ["production"]="production-service:8000" - ["suppliers"]="suppliers-service:8000" - ["pos"]="pos-service:8000" - ["external"]="city-service:8000" - ["forecasting"]="forecasting-service:8000" - ["training"]="training-service:8000" - ["alert-processor"]="alert-processor-service:8000" - ["notification"]="notification-service:8000" -) - -# Results tracking -TOTAL_SERVICES=12 -SUCCESSFUL_TESTS=0 -FAILED_TESTS=0 -declare -a FAILED_SERVICES - -# ============================================================================ -# Helper Functions -# ============================================================================ - -print_header() { - echo -e "${BLUE}============================================================================${NC}" - echo -e "${BLUE}$1${NC}" - echo -e "${BLUE}============================================================================${NC}" -} - -print_success() { - echo -e "${GREEN}✓${NC} $1" -} - -print_error() { - echo -e "${RED}✗${NC} $1" -} - -print_warning() { - echo -e "${YELLOW}⚠${NC} $1" -} - -print_info() { - echo -e "${BLUE}ℹ${NC} $1" -} - -# ============================================================================ -# Test Functions -# ============================================================================ - -test_service_preview() { - local service_name=$1 - local service_host=$2 - local endpoint_path=$3 - - echo "" - echo -e "${BLUE}Testing ${service_name}...${NC}" - - # Get running pod - local pod=$(kubectl get pods -n bakery-ia -l app=${service_name}-service 2>/dev/null | grep Running | head -1 | awk '{print $1}') - - if [ -z "$pod" ]; then - print_error "No running pod found for ${service_name}" - FAILED_TESTS=$((FAILED_TESTS + 1)) - FAILED_SERVICES+=("${service_name}") - return 1 - fi - - print_info "Pod: ${pod}" - - # Execute request inside pod - local response=$(kubectl exec -n bakery-ia "$pod" -- curl -s -w "\n%{http_code}" \ - -H "Authorization: Bearer ${SERVICE_TOKEN}" \ - "http://localhost:8000${endpoint_path}/tenant/${TENANT_ID}/deletion-preview" 2>&1) - - local http_code=$(echo "$response" | tail -1) - local body=$(echo "$response" | sed '$d') - - if [ "$http_code" = "200" ]; then - print_success "Preview successful (HTTP ${http_code})" - - # Parse and display counts - local total_records=$(echo "$body" | grep -o '"total_records":[0-9]*' | cut -d':' -f2 || echo "0") - print_info "Records to delete: ${total_records}" - - # Show breakdown if available - echo "$body" | python3 -m json.tool 2>/dev/null | grep -A50 "breakdown" | head -20 || echo "" - - SUCCESSFUL_TESTS=$((SUCCESSFUL_TESTS + 1)) - return 0 - elif [ "$http_code" = "401" ]; then - print_error "Authentication failed (HTTP ${http_code})" - print_warning "Service token may be invalid or expired" - echo "$body" - FAILED_TESTS=$((FAILED_TESTS + 1)) - FAILED_SERVICES+=("${service_name}") - return 1 - elif [ "$http_code" = "403" ]; then - print_error "Authorization failed (HTTP ${http_code})" - print_warning "Service token not recognized as service" - echo "$body" - FAILED_TESTS=$((FAILED_TESTS + 1)) - FAILED_SERVICES+=("${service_name}") - return 1 - elif [ "$http_code" = "404" ]; then - print_error "Endpoint not found (HTTP ${http_code})" - print_warning "Deletion endpoint may not be implemented" - FAILED_TESTS=$((FAILED_TESTS + 1)) - FAILED_SERVICES+=("${service_name}") - return 1 - elif [ "$http_code" = "500" ]; then - print_error "Server error (HTTP ${http_code})" - echo "$body" | head -5 - FAILED_TESTS=$((FAILED_TESTS + 1)) - FAILED_SERVICES+=("${service_name}") - return 1 - else - print_error "Unexpected response (HTTP ${http_code})" - echo "$body" | head -5 - FAILED_TESTS=$((FAILED_TESTS + 1)) - FAILED_SERVICES+=("${service_name}") - return 1 - fi -} - -test_service_deletion() { - local service_name=$1 - local service_host=$2 - local endpoint_path=$3 - - echo "" - echo -e "${BLUE}Deleting data in ${service_name}...${NC}" - - # Get running pod - local pod=$(kubectl get pods -n bakery-ia -l app=${service_name}-service 2>/dev/null | grep Running | head -1 | awk '{print $1}') - - if [ -z "$pod" ]; then - print_error "No running pod found for ${service_name}" - FAILED_TESTS=$((FAILED_TESTS + 1)) - FAILED_SERVICES+=("${service_name}") - return 1 - fi - - # Execute deletion request inside pod - local response=$(kubectl exec -n bakery-ia "$pod" -- curl -s -w "\n%{http_code}" \ - -X DELETE \ - -H "Authorization: Bearer ${SERVICE_TOKEN}" \ - "http://localhost:8000${endpoint_path}/tenant/${TENANT_ID}" 2>&1) - - local http_code=$(echo "$response" | tail -1) - local body=$(echo "$response" | sed '$d') - - if [ "$http_code" = "200" ]; then - print_success "Deletion successful (HTTP ${http_code})" - - # Parse and display deletion summary - local total_deleted=$(echo "$body" | grep -o '"total_records_deleted":[0-9]*' | cut -d':' -f2 || echo "0") - print_info "Records deleted: ${total_deleted}" - - SUCCESSFUL_TESTS=$((SUCCESSFUL_TESTS + 1)) - return 0 - else - print_error "Deletion failed (HTTP ${http_code})" - echo "$body" | head -5 - FAILED_TESTS=$((FAILED_TESTS + 1)) - FAILED_SERVICES+=("${service_name}") - return 1 - fi -} - -# ============================================================================ -# Main Test Execution -# ============================================================================ - -main() { - print_header "Tenant Deletion System - Functional Test" - - echo "" - print_info "Tenant ID: ${TENANT_ID}" - print_info "Test Mode: ${TEST_MODE}" - print_info "Services to test: ${TOTAL_SERVICES}" - echo "" - - # Verify service token - print_info "Verifying service token..." - if python scripts/generate_service_token.py --verify "${SERVICE_TOKEN}" > /dev/null 2>&1; then - print_success "Service token is valid" - else - print_error "Service token is invalid or expired" - exit 1 - fi - - echo "" - print_header "Phase 1: Testing Service Previews" - - # Test each service preview - test_service_preview "orders" "orders-service:8000" "/api/v1/orders" - test_service_preview "inventory" "inventory-service:8000" "/api/v1/inventory" - test_service_preview "recipes" "recipes-service:8000" "/api/v1/recipes" - test_service_preview "sales" "sales-service:8000" "/api/v1/sales" - test_service_preview "production" "production-service:8000" "/api/v1/production" - test_service_preview "suppliers" "suppliers-service:8000" "/api/v1/suppliers" - test_service_preview "pos" "pos-service:8000" "/api/v1/pos" - test_service_preview "external" "city-service:8000" "/api/v1/nominatim" - test_service_preview "forecasting" "forecasting-service:8000" "/api/v1/forecasting" - test_service_preview "training" "training-service:8000" "/api/v1/training" - test_service_preview "alert-processor" "alert-processor-service:8000" "/api/v1/analytics" - test_service_preview "notification" "notification-service:8000" "/api/v1/notifications" - - # Summary - echo "" - print_header "Preview Test Results" - echo -e "Total Services: ${TOTAL_SERVICES}" - echo -e "${GREEN}Successful:${NC} ${SUCCESSFUL_TESTS}/${TOTAL_SERVICES}" - echo -e "${RED}Failed:${NC} ${FAILED_TESTS}/${TOTAL_SERVICES}" - - if [ ${FAILED_TESTS} -gt 0 ]; then - echo "" - print_warning "Failed Services:" - for service in "${FAILED_SERVICES[@]}"; do - echo " - ${service}" - done - fi - - # Ask for confirmation before actual deletion - if [ "$TEST_MODE" = "delete" ]; then - echo "" - print_header "Phase 2: Actual Deletion" - print_warning "This will PERMANENTLY delete data for tenant ${TENANT_ID}" - print_warning "This operation is IRREVERSIBLE" - echo "" - read -p "Are you sure you want to proceed? (yes/no): " confirm - - if [ "$confirm" != "yes" ]; then - print_info "Deletion cancelled by user" - exit 0 - fi - - # Reset counters - SUCCESSFUL_TESTS=0 - FAILED_TESTS=0 - FAILED_SERVICES=() - - # Execute deletions - test_service_deletion "orders" "orders-service:8000" "/api/v1/orders" - test_service_deletion "inventory" "inventory-service:8000" "/api/v1/inventory" - test_service_deletion "recipes" "recipes-service:8000" "/api/v1/recipes" - test_service_deletion "sales" "sales-service:8000" "/api/v1/sales" - test_service_deletion "production" "production-service:8000" "/api/v1/production" - test_service_deletion "suppliers" "suppliers-service:8000" "/api/v1/suppliers" - test_service_deletion "pos" "pos-service:8000" "/api/v1/pos" - test_service_deletion "external" "city-service:8000" "/api/v1/nominatim" - test_service_deletion "forecasting" "forecasting-service:8000" "/api/v1/forecasting" - test_service_deletion "training" "training-service:8000" "/api/v1/training" - test_service_deletion "alert-processor" "alert-processor-service:8000" "/api/v1/analytics" - test_service_deletion "notification" "notification-service:8000" "/api/v1/notifications" - - # Deletion summary - echo "" - print_header "Deletion Test Results" - echo -e "Total Services: ${TOTAL_SERVICES}" - echo -e "${GREEN}Successful:${NC} ${SUCCESSFUL_TESTS}/${TOTAL_SERVICES}" - echo -e "${RED}Failed:${NC} ${FAILED_TESTS}/${TOTAL_SERVICES}" - - if [ ${FAILED_TESTS} -gt 0 ]; then - echo "" - print_warning "Failed Services:" - for service in "${FAILED_SERVICES[@]}"; do - echo " - ${service}" - done - fi - fi - - echo "" - print_header "Test Complete" - - if [ ${FAILED_TESTS} -eq 0 ]; then - print_success "All tests passed successfully!" - exit 0 - else - print_error "Some tests failed. See details above." - exit 1 - fi -} - -# Run main function -main diff --git a/scripts/functional_test_deletion_simple.sh b/scripts/functional_test_deletion_simple.sh deleted file mode 100755 index 08221e16..00000000 --- a/scripts/functional_test_deletion_simple.sh +++ /dev/null @@ -1,145 +0,0 @@ -#!/bin/bash -# ============================================================================ -# Functional Test: Tenant Deletion System (Simple Version) -# ============================================================================ - -set +e # Don't exit on error - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' - -# Configuration -TENANT_ID="${1:-dbc2128a-7539-470c-94b9-c1e37031bd77}" - -# Generate or use provided SERVICE_TOKEN -if [ -z "${SERVICE_TOKEN}" ]; then - SERVICE_TOKEN=$(python3 scripts/generate_service_token.py tenant-deletion-orchestrator 2>&1 | grep -A1 "Token:" | tail -1 | sed 's/^[[:space:]]*//' | tr -d '\n') -else - # Clean the token if provided (remove whitespace and newlines) - SERVICE_TOKEN=$(echo "${SERVICE_TOKEN}" | tr -d '[:space:]') -fi - -# Results -TOTAL_SERVICES=12 -SUCCESSFUL_TESTS=0 -FAILED_TESTS=0 - -# Helper functions -print_header() { - echo -e "${BLUE}================================================================================${NC}" - echo -e "${BLUE}$1${NC}" - echo -e "${BLUE}================================================================================${NC}" -} - -print_success() { - echo -e "${GREEN}✓${NC} $1" -} - -print_error() { - echo -e "${RED}✗${NC} $1" -} - -print_info() { - echo -e "${BLUE}ℹ${NC} $1" -} - -# Test function -test_service() { - local service_name=$1 - local endpoint_path=$2 - local port=${3:-8000} # Default to 8000 if not specified - - echo "" - echo -e "${BLUE}Testing ${service_name}...${NC}" - - # Find running pod - local pod=$(kubectl get pods -n bakery-ia 2>/dev/null | grep "${service_name}" | grep "Running" | grep "1/1" | head -1 | awk '{print $1}') - - if [ -z "$pod" ]; then - print_error "No running pod found" - FAILED_TESTS=$((FAILED_TESTS + 1)) - return 1 - fi - - print_info "Pod: ${pod}" - - # Execute request - local result=$(kubectl exec -n bakery-ia "$pod" -- curl -s -w "\nHTTP_CODE:%{http_code}" \ - -H "Authorization: Bearer ${SERVICE_TOKEN}" \ - "http://localhost:${port}${endpoint_path}/tenant/${TENANT_ID}/deletion-preview" 2>&1) - - local http_code=$(echo "$result" | grep "HTTP_CODE" | cut -d':' -f2) - local body=$(echo "$result" | sed '/HTTP_CODE/d') - - if [ "$http_code" = "200" ]; then - print_success "Preview successful (HTTP ${http_code})" - local total=$(echo "$body" | grep -o '"total_records":[0-9]*' | cut -d':' -f2 | head -1) - if [ -n "$total" ]; then - print_info "Records to delete: ${total}" - fi - SUCCESSFUL_TESTS=$((SUCCESSFUL_TESTS + 1)) - return 0 - elif [ "$http_code" = "401" ]; then - print_error "Authentication failed (HTTP ${http_code})" - FAILED_TESTS=$((FAILED_TESTS + 1)) - return 1 - elif [ "$http_code" = "403" ]; then - print_error "Authorization failed (HTTP ${http_code})" - FAILED_TESTS=$((FAILED_TESTS + 1)) - return 1 - elif [ "$http_code" = "404" ]; then - print_error "Endpoint not found (HTTP ${http_code})" - FAILED_TESTS=$((FAILED_TESTS + 1)) - return 1 - elif [ "$http_code" = "500" ]; then - print_error "Server error (HTTP ${http_code})" - echo "$body" | head -3 - FAILED_TESTS=$((FAILED_TESTS + 1)) - return 1 - else - print_error "Unexpected response (HTTP ${http_code})" - FAILED_TESTS=$((FAILED_TESTS + 1)) - return 1 - fi -} - -# Main -print_header "Tenant Deletion System - Functional Test" -echo "" -print_info "Tenant ID: ${TENANT_ID}" -print_info "Services to test: ${TOTAL_SERVICES}" -echo "" - -# Test all services -test_service "orders-service" "/api/v1/orders" -test_service "inventory-service" "/api/v1/inventory" -test_service "recipes-service" "/api/v1/recipes" -test_service "sales-service" "/api/v1/sales" -test_service "production-service" "/api/v1/production" -test_service "suppliers-service" "/api/v1/suppliers" -test_service "pos-service" "/api/v1/pos" -test_service "external-service" "/api/v1/external" -test_service "forecasting-service" "/api/v1/forecasting" -test_service "training-service" "/api/v1/training" -test_service "alert-processor-api" "/api/v1/alerts" 8010 -test_service "notification-service" "/api/v1/notification" - -# Summary -echo "" -print_header "Test Results" -echo "Total Services: ${TOTAL_SERVICES}" -echo -e "${GREEN}Successful:${NC} ${SUCCESSFUL_TESTS}/${TOTAL_SERVICES}" -echo -e "${RED}Failed:${NC} ${FAILED_TESTS}/${TOTAL_SERVICES}" -echo "" - -if [ ${FAILED_TESTS} -eq 0 ]; then - print_success "All tests passed!" - exit 0 -else - print_error "Some tests failed" - exit 1 -fi diff --git a/scripts/generate_audit_endpoints.py b/scripts/generate_audit_endpoints.py deleted file mode 100644 index 4a2d50ae..00000000 --- a/scripts/generate_audit_endpoints.py +++ /dev/null @@ -1,281 +0,0 @@ -#!/usr/bin/env python3 -""" -Script to generate audit.py endpoint files for all services. -This ensures consistency across all microservices. -""" - -import os -from pathlib import Path - -# Template for audit.py file -AUDIT_TEMPLATE = """# services/{service}/app/api/audit.py -\"\"\" -Audit Logs API - Retrieve audit trail for {service} service -\"\"\" - -from fastapi import APIRouter, Depends, HTTPException, Query, Path, status -from typing import Optional, Dict, Any -from uuid import UUID -from datetime import datetime -import structlog -from sqlalchemy import select, func, and_ -from sqlalchemy.ext.asyncio import AsyncSession - -from app.models import AuditLog -from shared.auth.decorators import get_current_user_dep -from shared.auth.access_control import require_user_role -from shared.routing import RouteBuilder -from shared.models.audit_log_schemas import ( - AuditLogResponse, - AuditLogListResponse, - AuditLogStatsResponse -) -from app.core.database import database_manager - -route_builder = RouteBuilder('{service_route}') -router = APIRouter(tags=["audit-logs"]) -logger = structlog.get_logger() - - -async def get_db(): - \"\"\"Database session dependency\"\"\" - async with database_manager.get_session() as session: - yield session - - -@router.get( - route_builder.build_base_route("audit-logs"), - response_model=AuditLogListResponse -) -@require_user_role(['admin', 'owner']) -async def get_audit_logs( - tenant_id: UUID = Path(..., description="Tenant ID"), - start_date: Optional[datetime] = Query(None, description="Filter logs from this date"), - end_date: Optional[datetime] = Query(None, description="Filter logs until this date"), - user_id: Optional[UUID] = Query(None, description="Filter by user ID"), - action: Optional[str] = Query(None, description="Filter by action type"), - resource_type: Optional[str] = Query(None, description="Filter by resource type"), - severity: Optional[str] = Query(None, description="Filter by severity level"), - search: Optional[str] = Query(None, description="Search in description field"), - limit: int = Query(100, ge=1, le=1000, description="Number of records to return"), - offset: int = Query(0, ge=0, description="Number of records to skip"), - current_user: Dict[str, Any] = Depends(get_current_user_dep), - db: AsyncSession = Depends(get_db) -): - \"\"\" - Get audit logs for {service} service. - Requires admin or owner role. - \"\"\" - try: - logger.info( - "Retrieving audit logs", - tenant_id=tenant_id, - user_id=current_user.get("user_id"), - filters={{ - "start_date": start_date, - "end_date": end_date, - "action": action, - "resource_type": resource_type, - "severity": severity - }} - ) - - # Build query filters - filters = [AuditLog.tenant_id == tenant_id] - - if start_date: - filters.append(AuditLog.created_at >= start_date) - if end_date: - filters.append(AuditLog.created_at <= end_date) - if user_id: - filters.append(AuditLog.user_id == user_id) - if action: - filters.append(AuditLog.action == action) - if resource_type: - filters.append(AuditLog.resource_type == resource_type) - if severity: - filters.append(AuditLog.severity == severity) - if search: - filters.append(AuditLog.description.ilike(f"%{{search}}%")) - - # Count total matching records - count_query = select(func.count()).select_from(AuditLog).where(and_(*filters)) - total_result = await db.execute(count_query) - total = total_result.scalar() or 0 - - # Fetch paginated results - query = ( - select(AuditLog) - .where(and_(*filters)) - .order_by(AuditLog.created_at.desc()) - .limit(limit) - .offset(offset) - ) - - result = await db.execute(query) - audit_logs = result.scalars().all() - - # Convert to response models - items = [AuditLogResponse.from_orm(log) for log in audit_logs] - - logger.info( - "Successfully retrieved audit logs", - tenant_id=tenant_id, - total=total, - returned=len(items) - ) - - return AuditLogListResponse( - items=items, - total=total, - limit=limit, - offset=offset, - has_more=(offset + len(items)) < total - ) - - except Exception as e: - logger.error( - "Failed to retrieve audit logs", - error=str(e), - tenant_id=tenant_id - ) - raise HTTPException( - status_code=500, - detail=f"Failed to retrieve audit logs: {{str(e)}}" - ) - - -@router.get( - route_builder.build_base_route("audit-logs/stats"), - response_model=AuditLogStatsResponse -) -@require_user_role(['admin', 'owner']) -async def get_audit_log_stats( - tenant_id: UUID = Path(..., description="Tenant ID"), - start_date: Optional[datetime] = Query(None, description="Filter logs from this date"), - end_date: Optional[datetime] = Query(None, description="Filter logs until this date"), - current_user: Dict[str, Any] = Depends(get_current_user_dep), - db: AsyncSession = Depends(get_db) -): - \"\"\" - Get audit log statistics for {service} service. - Requires admin or owner role. - \"\"\" - try: - logger.info( - "Retrieving audit log statistics", - tenant_id=tenant_id, - user_id=current_user.get("user_id") - ) - - # Build base filters - filters = [AuditLog.tenant_id == tenant_id] - if start_date: - filters.append(AuditLog.created_at >= start_date) - if end_date: - filters.append(AuditLog.created_at <= end_date) - - # Total events - count_query = select(func.count()).select_from(AuditLog).where(and_(*filters)) - total_result = await db.execute(count_query) - total_events = total_result.scalar() or 0 - - # Events by action - action_query = ( - select(AuditLog.action, func.count().label('count')) - .where(and_(*filters)) - .group_by(AuditLog.action) - ) - action_result = await db.execute(action_query) - events_by_action = {{row.action: row.count for row in action_result}} - - # Events by severity - severity_query = ( - select(AuditLog.severity, func.count().label('count')) - .where(and_(*filters)) - .group_by(AuditLog.severity) - ) - severity_result = await db.execute(severity_query) - events_by_severity = {{row.severity: row.count for row in severity_result}} - - # Events by resource type - resource_query = ( - select(AuditLog.resource_type, func.count().label('count')) - .where(and_(*filters)) - .group_by(AuditLog.resource_type) - ) - resource_result = await db.execute(resource_query) - events_by_resource_type = {{row.resource_type: row.count for row in resource_result}} - - # Date range - date_range_query = ( - select( - func.min(AuditLog.created_at).label('min_date'), - func.max(AuditLog.created_at).label('max_date') - ) - .where(and_(*filters)) - ) - date_result = await db.execute(date_range_query) - date_row = date_result.one() - - logger.info( - "Successfully retrieved audit log statistics", - tenant_id=tenant_id, - total_events=total_events - ) - - return AuditLogStatsResponse( - total_events=total_events, - events_by_action=events_by_action, - events_by_severity=events_by_severity, - events_by_resource_type=events_by_resource_type, - date_range={{ - "min": date_row.min_date, - "max": date_row.max_date - }} - ) - - except Exception as e: - logger.error( - "Failed to retrieve audit log statistics", - error=str(e), - tenant_id=tenant_id - ) - raise HTTPException( - status_code=500, - detail=f"Failed to retrieve audit log statistics: {{str(e)}}" - ) -""" - -# Services to generate for (excluding sales and inventory which are already done) -SERVICES = [ - ('orders', 'orders'), - ('production', 'production'), - ('recipes', 'recipes'), - ('suppliers', 'suppliers'), - ('pos', 'pos'), - ('training', 'training'), - ('notification', 'notification'), - ('external', 'external'), - ('forecasting', 'forecasting'), -] - -def main(): - base_path = Path(__file__).parent.parent / "services" - - for service_name, route_name in SERVICES: - service_path = base_path / service_name / "app" / "api" - audit_file = service_path / "audit.py" - - # Create the file - content = AUDIT_TEMPLATE.format( - service=service_name, - service_route=route_name - ) - - audit_file.write_text(content) - print(f"✓ Created {audit_file}") - -if __name__ == "__main__": - main() - print("\n✓ All audit endpoint files generated successfully!") diff --git a/scripts/generate_child_auth_files.py b/scripts/generate_child_auth_files.py deleted file mode 100755 index 5da6d8b7..00000000 --- a/scripts/generate_child_auth_files.py +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env python3 -""" -Generate Child Auth Files Script - -Creates auth.json files for each child tenant with appropriate users. -""" - -import json -import os -from pathlib import Path -from datetime import datetime, timedelta -import uuid - -def generate_child_auth_file(tenant_id: str, tenant_name: str, parent_tenant_id: str) -> dict: - """Generate auth.json data for a child tenant""" - - # Generate user IDs based on tenant ID - manager_id = str(uuid.uuid5(uuid.NAMESPACE_DNS, f"manager-{tenant_id}")) - staff_id = str(uuid.uuid5(uuid.NAMESPACE_DNS, f"staff-{tenant_id}")) - - # Create users - users = [ - { - "id": manager_id, - "tenant_id": tenant_id, - "name": f"Gerente {tenant_name}", - "email": f"gerente.{tenant_id.lower()}@panaderiaartesana.es", - "role": "manager", - "is_active": True, - "created_at": "BASE_TS - 180d", - "updated_at": "BASE_TS - 180d" - }, - { - "id": staff_id, - "tenant_id": tenant_id, - "name": f"Empleado {tenant_name}", - "email": f"empleado.{tenant_id.lower()}@panaderiaartesana.es", - "role": "user", - "is_active": True, - "created_at": "BASE_TS - 150d", - "updated_at": "BASE_TS - 150d" - } - ] - - return {"users": users} - -def main(): - """Main function to generate auth files for all child tenants""" - print("=== Generating Child Auth Files ===") - - base_path = Path("shared/demo/fixtures/enterprise") - children_dir = base_path / "children" - - # Get parent tenant info - parent_tenant_file = base_path / "parent" / "01-tenant.json" - with open(parent_tenant_file, 'r', encoding='utf-8') as f: - parent_data = json.load(f) - - parent_tenant_id = parent_data["tenant"]["id"] - - # Process each child directory - for child_dir in children_dir.iterdir(): - if child_dir.is_dir(): - tenant_id = child_dir.name - - # Get tenant info from child's tenant.json - child_tenant_file = child_dir / "01-tenant.json" - if child_tenant_file.exists(): - with open(child_tenant_file, 'r', encoding='utf-8') as f: - tenant_data = json.load(f) - - # Child files have location data, not tenant data - tenant_name = tenant_data["location"]["name"] - - # Generate auth data - auth_data = generate_child_auth_file(tenant_id, tenant_name, parent_tenant_id) - - # Write auth.json file - auth_file = child_dir / "02-auth.json" - with open(auth_file, 'w', encoding='utf-8') as f: - json.dump(auth_data, f, ensure_ascii=False, indent=2) - - print(f"✓ Generated {auth_file}") - else: - print(f"✗ Missing tenant.json in {child_dir}") - - print("\n=== Auth File Generation Complete ===") - -if __name__ == "__main__": - main() diff --git a/scripts/generate_deletion_service.py b/scripts/generate_deletion_service.py deleted file mode 100644 index ae68b252..00000000 --- a/scripts/generate_deletion_service.py +++ /dev/null @@ -1,270 +0,0 @@ -#!/usr/bin/env python3 -""" -Quick script to generate deletion service boilerplate -Usage: python generate_deletion_service.py -Example: python generate_deletion_service.py pos POSConfiguration,POSTransaction,POSSession -""" - -import sys -import os -from pathlib import Path - - -def generate_deletion_service(service_name: str, models: list[str]): - """Generate deletion service file from template""" - - service_class = f"{service_name.title().replace('_', '')}TenantDeletionService" - model_imports = ", ".join(models) - - # Build preview section - preview_code = [] - delete_code = [] - - for i, model in enumerate(models): - model_lower = model.lower().replace('_', ' ') - model_plural = f"{model_lower}s" if not model_lower.endswith('s') else model_lower - - preview_code.append(f""" - # Count {model_plural} - try: - {model.lower()}_count = await self.db.scalar( - select(func.count({model}.id)).where({model}.tenant_id == tenant_id) - ) - preview["{model_plural}"] = {model.lower()}_count or 0 - except Exception: - preview["{model_plural}"] = 0 # Table might not exist -""") - - delete_code.append(f""" - # Delete {model_plural} - try: - {model.lower()}_delete = await self.db.execute( - delete({model}).where({model}.tenant_id == tenant_id) - ) - result.add_deleted_items("{model_plural}", {model.lower()}_delete.rowcount) - - logger.info("Deleted {model_plural} for tenant", - tenant_id=tenant_id, - count={model.lower()}_delete.rowcount) - - except Exception as e: - logger.error("Error deleting {model_plural}", - tenant_id=tenant_id, - error=str(e)) - result.add_error(f"{model} deletion: {{str(e)}}") -""") - - template = f'''""" -{service_name.title()} Service - Tenant Data Deletion -Handles deletion of all {service_name}-related data for a tenant -""" -from typing import Dict -from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy import select, delete, func -import structlog - -from shared.services.tenant_deletion import BaseTenantDataDeletionService, TenantDataDeletionResult - -logger = structlog.get_logger() - - -class {service_class}(BaseTenantDataDeletionService): - """Service for deleting all {service_name}-related data for a tenant""" - - def __init__(self, db_session: AsyncSession): - super().__init__("{service_name}-service") - self.db = db_session - - async def get_tenant_data_preview(self, tenant_id: str) -> Dict[str, int]: - """Get counts of what would be deleted""" - - try: - preview = {{}} - - # Import models here to avoid circular imports - from app.models import {model_imports} -{"".join(preview_code)} - return preview - - except Exception as e: - logger.error("Error getting deletion preview", - tenant_id=tenant_id, - error=str(e)) - return {{}} - - async def delete_tenant_data(self, tenant_id: str) -> TenantDataDeletionResult: - """Delete all data for a tenant""" - - result = TenantDataDeletionResult(tenant_id, self.service_name) - - try: - # Import models here to avoid circular imports - from app.models import {model_imports} -{"".join(delete_code)} - # Commit all deletions - await self.db.commit() - - logger.info("Tenant data deletion completed", - tenant_id=tenant_id, - deleted_counts=result.deleted_counts) - - except Exception as e: - logger.error("Fatal error during tenant data deletion", - tenant_id=tenant_id, - error=str(e)) - await self.db.rollback() - result.add_error(f"Fatal error: {{str(e)}}") - - return result -''' - - return template - - -def generate_api_endpoints(service_name: str): - """Generate API endpoint code""" - - service_class = f"{service_name.title().replace('_', '')}TenantDeletionService" - - template = f''' -# ===== Tenant Data Deletion Endpoints ===== - -@router.delete("/tenant/{{tenant_id}}") -async def delete_tenant_data( - tenant_id: str, - current_user: dict = Depends(get_current_user_dep), - db: AsyncSession = Depends(get_db) -): - """ - Delete all {service_name}-related data for a tenant - Only accessible by internal services (called during tenant deletion) - """ - - logger.info(f"Tenant data deletion request received for tenant: {{tenant_id}}") - - # Only allow internal service calls - if current_user.get("type") != "service": - raise HTTPException( - status_code=403, - detail="This endpoint is only accessible to internal services" - ) - - try: - from app.services.tenant_deletion_service import {service_class} - - deletion_service = {service_class}(db) - result = await deletion_service.safe_delete_tenant_data(tenant_id) - - return {{ - "message": "Tenant data deletion completed in {service_name}-service", - "summary": result.to_dict() - }} - - except Exception as e: - logger.error(f"Tenant data deletion failed for {{tenant_id}}: {{e}}") - raise HTTPException( - status_code=500, - detail=f"Failed to delete tenant data: {{str(e)}}" - ) - - -@router.get("/tenant/{{tenant_id}}/deletion-preview") -async def preview_tenant_data_deletion( - tenant_id: str, - current_user: dict = Depends(get_current_user_dep), - db: AsyncSession = Depends(get_db) -): - """ - Preview what data would be deleted for a tenant (dry-run) - Accessible by internal services and tenant admins - """ - - # Allow internal services and admins - is_service = current_user.get("type") == "service" - is_admin = current_user.get("role") in ["owner", "admin"] - - if not (is_service or is_admin): - raise HTTPException( - status_code=403, - detail="Insufficient permissions" - ) - - try: - from app.services.tenant_deletion_service import {service_class} - - deletion_service = {service_class}(db) - preview = await deletion_service.get_tenant_data_preview(tenant_id) - - return {{ - "tenant_id": tenant_id, - "service": "{service_name}-service", - "data_counts": preview, - "total_items": sum(preview.values()) - }} - - except Exception as e: - logger.error(f"Deletion preview failed for {{tenant_id}}: {{e}}") - raise HTTPException( - status_code=500, - detail=f"Failed to get deletion preview: {{str(e)}}" - ) -''' - - return template - - -def main(): - if len(sys.argv) < 3: - print("Usage: python generate_deletion_service.py ") - print("Example: python generate_deletion_service.py pos POSConfiguration,POSTransaction,POSSession") - sys.exit(1) - - service_name = sys.argv[1] - models = [m.strip() for m in sys.argv[2].split(',')] - - # Generate service file - service_code = generate_deletion_service(service_name, models) - - # Generate API endpoints - api_code = generate_api_endpoints(service_name) - - # Output files - service_dir = Path(f"services/{service_name}/app/services") - - print(f"\n{'='*80}") - print(f"Generated code for {service_name} service with models: {', '.join(models)}") - print(f"{'='*80}\n") - - print("1. DELETION SERVICE FILE:") - print(f" Location: {service_dir}/tenant_deletion_service.py") - print("-" * 80) - print(service_code) - print() - - print("\n2. API ENDPOINTS TO ADD:") - print(f" Add to: services/{service_name}/app/api/.py") - print("-" * 80) - print(api_code) - print() - - # Optionally write files - write = input("\nWrite files to disk? (y/n): ").lower().strip() - if write == 'y': - # Create service file - service_dir.mkdir(parents=True, exist_ok=True) - service_file = service_dir / "tenant_deletion_service.py" - - with open(service_file, 'w') as f: - f.write(service_code) - - print(f"\n✅ Created: {service_file}") - print(f"\n⚠️ Next steps:") - print(f" 1. Review and customize {service_file}") - print(f" 2. Add the API endpoints to services/{service_name}/app/api/.py") - print(f" 3. Test with: curl -X GET 'http://localhost:8000/api/v1/{service_name}/tenant/{{id}}/deletion-preview'") - else: - print("\n✅ Files not written. Copy the code above manually.") - - -if __name__ == "__main__": - main() diff --git a/scripts/generate_demo_data_improved.py b/scripts/generate_demo_data_improved.py deleted file mode 100644 index 087be4bc..00000000 --- a/scripts/generate_demo_data_improved.py +++ /dev/null @@ -1,950 +0,0 @@ -#!/usr/bin/env python3 -""" -Bakery-IA Demo Data Generator - Improved Version -Generates hyper-realistic, deterministic demo seed data for Professional tier. - -This script addresses all issues identified in the analysis report: -- Complete inventory with all ingredients and stock entries -- Production consumption calculations aligned with inventory -- Sales data aligned with completed batches -- Forecasting with 88-92% accuracy -- Cross-reference validation -- Edge case scenarios maintained - -Usage: - python generate_demo_data_improved.py - -Output: - - Updated JSON files in shared/demo/fixtures/professional/ - - Validation report in DEMO_DATA_GENERATION_REPORT.md - - Cross-reference validation -""" - -import json -import random -import uuid -from datetime import datetime, timedelta -from pathlib import Path -from typing import Dict, List, Any, Tuple -from collections import defaultdict -import copy - -# ============================================================================ -# CONFIGURATION -# ============================================================================ - -# Base timestamp for all relative dates -BASE_TS = datetime(2025, 1, 15, 6, 0, 0) # 2025-01-15T06:00:00Z - -# Deterministic seed for reproducibility -RANDOM_SEED = 42 -random.seed(RANDOM_SEED) - -# Paths -BASE_DIR = Path(__file__).parent -FIXTURES_DIR = BASE_DIR / "shared" / "demo" / "fixtures" / "professional" -METADATA_DIR = BASE_DIR / "shared" / "demo" / "metadata" - -# Tenant ID -TENANT_ID = "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6" - -# ============================================================================ -# UTILITY FUNCTIONS -# ============================================================================ - -def format_timestamp(dt: datetime) -> str: - """Format datetime as ISO 8601 string.""" - return dt.strftime("%Y-%m-%dT%H:%M:%SZ") - -def parse_offset(offset_str: str) -> timedelta: - """Parse offset string like 'BASE_TS - 7d 6h' or 'BASE_TS + 1h30m' to timedelta.""" - if not offset_str or offset_str == "BASE_TS": - return timedelta(0) - - # Remove 'BASE_TS' and strip - offset_str = offset_str.replace("BASE_TS", "").strip() - - sign = 1 - if offset_str.startswith("-"): - sign = -1 - offset_str = offset_str[1:].strip() - elif offset_str.startswith("+"): - offset_str = offset_str[1:].strip() - - delta = timedelta(0) - - # Handle combined formats like "1h30m" - import re - - # Extract days - day_match = re.search(r'(\d+(?:\.\d+)?)d', offset_str) - if day_match: - delta += timedelta(days=float(day_match.group(1))) - - # Extract hours - hour_match = re.search(r'(\d+(?:\.\d+)?)h', offset_str) - if hour_match: - delta += timedelta(hours=float(hour_match.group(1))) - - # Extract minutes - min_match = re.search(r'(\d+(?:\.\d+)?)m', offset_str) - if min_match: - delta += timedelta(minutes=float(min_match.group(1))) - - return delta * sign - -def calculate_timestamp(offset_str: str) -> str: - """Calculate timestamp from BASE_TS with offset.""" - delta = parse_offset(offset_str) - result = BASE_TS + delta - return format_timestamp(result) - -def parse_timestamp_flexible(ts_str: str) -> datetime: - """Parse timestamp that could be ISO format or BASE_TS + offset.""" - if not ts_str: - return BASE_TS - - if "BASE_TS" in ts_str: - delta = parse_offset(ts_str) - return BASE_TS + delta - - try: - return datetime.fromisoformat(ts_str.replace("Z", "+00:00")) - except ValueError: - return BASE_TS - -def load_json(filename: str) -> Dict: - """Load JSON file from fixtures directory.""" - path = FIXTURES_DIR / filename - if not path.exists(): - return {} - with open(path, 'r', encoding='utf-8') as f: - return json.load(f) - -def save_json(filename: str, data: Dict): - """Save JSON file to fixtures directory.""" - path = FIXTURES_DIR / filename - path.parent.mkdir(parents=True, exist_ok=True) - with open(path, 'w', encoding='utf-8') as f: - json.dump(data, f, indent=2, ensure_ascii=False) - -def generate_batch_number(sku: str, date: datetime) -> str: - """Generate unique batch number.""" - date_str = date.strftime("%Y%m%d") - sequence = random.randint(1, 999) - return f"{sku}-{date_str}-{sequence:03d}" - -def generate_po_number() -> str: - """Generate unique purchase order number.""" - year = BASE_TS.year - sequence = random.randint(1, 999) - return f"PO-{year}-{sequence:03d}" - -def generate_sales_id() -> str: - """Generate unique sales ID.""" - year = BASE_TS.year - month = BASE_TS.month - sequence = random.randint(1, 9999) - return f"SALES-{year}{month:02d}-{sequence:04d}" - -def generate_order_id() -> str: - """Generate unique order ID.""" - year = BASE_TS.year - sequence = random.randint(1, 9999) - return f"ORDER-{year}-{sequence:04d}" - -# ============================================================================ -# DATA GENERATORS -# ============================================================================ - -class DemoDataGenerator: - def __init__(self): - self.tenant_id = TENANT_ID - self.base_ts = BASE_TS - - # Load existing data - self.inventory_data = load_json("03-inventory.json") - self.recipes_data = load_json("04-recipes.json") - self.suppliers_data = load_json("05-suppliers.json") - self.production_data = load_json("06-production.json") - self.procurement_data = load_json("07-procurement.json") - self.orders_data = load_json("08-orders.json") - self.sales_data = load_json("09-sales.json") - self.forecasting_data = load_json("10-forecasting.json") - self.quality_data = load_json("12-quality.json") - self.orchestrator_data = load_json("11-orchestrator.json") - - # Cross-reference map - self.cross_refs = self._load_cross_refs() - - # Tracking - self.validation_errors = [] - self.validation_warnings = [] - self.changes = [] - self.stats = { - 'ingredients': 0, - 'stock_entries': 0, - 'batches': 0, - 'sales': 0, - 'forecasts': 0, - 'critical_stock': 0, - 'alerts': 0 - } - - def _load_cross_refs(self) -> Dict: - """Load cross-reference map.""" - path = METADATA_DIR / "cross_refs_map.json" - if path.exists(): - with open(path, 'r', encoding='utf-8') as f: - return json.load(f) - return {} - - def _add_validation_error(self, message: str): - """Add validation error.""" - self.validation_errors.append(message) - print(f"❌ ERROR: {message}") - - def _add_validation_warning(self, message: str): - """Add validation warning.""" - self.validation_warnings.append(message) - print(f"⚠️ WARNING: {message}") - - def _add_change(self, message: str): - """Add change log entry.""" - self.changes.append(message) - - # ======================================================================== - # INVENTORY GENERATION - # ======================================================================== - - def generate_complete_inventory(self): - """Generate complete inventory with all ingredients and stock entries.""" - print("📦 Generating complete inventory...") - - # Load existing ingredients - ingredients = self.inventory_data.get("ingredients", []) - existing_stock = self.inventory_data.get("stock", []) - - # Validate that all ingredients have stock entries - ingredient_ids = {ing["id"] for ing in ingredients} - stock_ingredient_ids = {stock["ingredient_id"] for stock in existing_stock} - - missing_stock = ingredient_ids - stock_ingredient_ids - if missing_stock: - self._add_validation_warning(f"Missing stock entries for {len(missing_stock)} ingredients") - - # Generate stock entries for missing ingredients - for ing_id in missing_stock: - # Find the ingredient - ingredient = next(ing for ing in ingredients if ing["id"] == ing_id) - - # Generate realistic stock entry - stock_entry = self._generate_stock_entry(ingredient) - existing_stock.append(stock_entry) - self._add_change(f"Generated stock entry for {ingredient['name']}") - - # Update inventory data - self.inventory_data["stock"] = existing_stock - self.stats["ingredients"] = len(ingredients) - self.stats["stock_entries"] = len(existing_stock) - - # Identify critical stock items - critical_count = 0 - for stock in existing_stock: - ingredient = next(ing for ing in ingredients if ing["id"] == stock["ingredient_id"]) - - if ingredient.get("reorder_point") and stock["current_quantity"] < ingredient["reorder_point"]: - critical_count += 1 - - # Check if there's a pending PO for this ingredient - has_po = self._has_pending_po(ingredient["id"]) - if not has_po: - self.stats["alerts"] += 1 - self._add_change(f"CRITICAL: {ingredient['name']} below reorder point with NO pending PO") - - self.stats["critical_stock"] = critical_count - print(f"✅ Generated complete inventory: {len(ingredients)} ingredients, {len(existing_stock)} stock entries") - print(f"✅ Critical stock items: {critical_count}") - - def _generate_stock_entry(self, ingredient: Dict) -> Dict: - """Generate realistic stock entry for an ingredient.""" - # Determine base quantity based on category - category = ingredient.get("ingredient_category", "OTHER") - - if category == "FLOUR": - base_qty = random.uniform(150, 300) - elif category == "DAIRY": - base_qty = random.uniform(50, 150) - elif category == "YEAST": - base_qty = random.uniform(5, 20) - else: - base_qty = random.uniform(20, 100) - - # Apply realistic variation - quantity = base_qty * random.uniform(0.8, 1.2) - - # Determine shelf life - if ingredient.get("is_perishable"): - shelf_life = random.randint(7, 30) - else: - shelf_life = random.randint(90, 180) - - # Generate batch number - sku = ingredient.get("sku", "GEN-001") - batch_date = self.base_ts - timedelta(days=random.randint(1, 14)) - batch_number = generate_batch_number(sku, batch_date) - - return { - "id": str(uuid.uuid4()), - "tenant_id": self.tenant_id, - "ingredient_id": ingredient["id"], - "current_quantity": round(quantity, 2), - "reserved_quantity": round(quantity * random.uniform(0.05, 0.15), 2), - "available_quantity": round(quantity * random.uniform(0.85, 0.95), 2), - "storage_location": self._get_storage_location(ingredient), - "production_stage": "raw_ingredient", - "quality_status": "good", - "expiration_date": calculate_timestamp(f"BASE_TS + {shelf_life}d"), - "supplier_id": self._get_supplier_for_ingredient(ingredient), - "batch_number": batch_number, - "created_at": calculate_timestamp(f"BASE_TS - {random.randint(1, 7)}d"), - "updated_at": "BASE_TS", - "is_available": True, - "is_expired": False - } - - def _get_supplier_for_ingredient(self, ingredient: Dict) -> str: - """Get appropriate supplier ID for ingredient.""" - category = ingredient.get("ingredient_category", "OTHER") - suppliers = self.suppliers_data.get("suppliers", []) - - # Map categories to suppliers - category_map = { - "FLOUR": "40000000-0000-0000-0000-000000000001", # Harinas del Norte - "DAIRY": "40000000-0000-0000-0000-000000000002", # Lácteos Gipuzkoa - "YEAST": "40000000-0000-0000-0000-000000000006", # Levaduras Spain - "SALT": "40000000-0000-0000-0000-000000000004", # Sal de Mar - } - - return category_map.get(category, suppliers[0]["id"] if suppliers else None) - - def _get_storage_location(self, ingredient: Dict) -> str: - """Get storage location based on ingredient type.""" - if ingredient.get("is_perishable"): - return "Almacén Refrigerado - Zona B" - else: - return "Almacén Principal - Zona A" - - def _has_pending_po(self, ingredient_id: str) -> bool: - """Check if there's a pending PO for this ingredient.""" - pos = self.procurement_data.get("purchase_orders", []) - - for po in pos: - if po["status"] in ["pending_approval", "confirmed", "in_transit"]: - for item in po.get("items", []): - if item.get("inventory_product_id") == ingredient_id: - return True - - return False - - # ======================================================================== - # PRODUCTION CONSUMPTION CALCULATIONS - # ======================================================================== - - def calculate_production_consumptions(self) -> List[Dict]: - """Calculate ingredient consumptions from completed batches.""" - print("🏭 Calculating production consumptions...") - - batches = self.production_data.get("batches", []) - recipes = {r["id"]: r for r in self.recipes_data.get("recipes", [])} - recipe_ingredients = self.recipes_data.get("recipe_ingredients", []) - - consumptions = [] - - for batch in batches: - if batch["status"] not in ["COMPLETED", "QUARANTINED"]: - continue - - recipe_id = batch.get("recipe_id") - if not recipe_id or recipe_id not in recipes: - continue - - recipe = recipes[recipe_id] - actual_qty = batch.get("actual_quantity", 0) - yield_qty = recipe.get("yield_quantity", 1) - - if yield_qty == 0: - continue - - scale_factor = actual_qty / yield_qty - - # Get ingredients for this recipe - ingredients = [ri for ri in recipe_ingredients if ri["recipe_id"] == recipe_id] - - for ing in ingredients: - ing_id = ing["ingredient_id"] - ing_qty = ing["quantity"] # in grams or ml - - # Convert to base unit (kg or L) - unit = ing.get("unit", "g") - if unit in ["g", "ml"]: - ing_qty_base = ing_qty / 1000.0 - else: - ing_qty_base = ing_qty - - consumed = ing_qty_base * scale_factor - - consumptions.append({ - "batch_id": batch["id"], - "batch_number": batch["batch_number"], - "ingredient_id": ing_id, - "quantity_consumed": round(consumed, 2), - "timestamp": batch.get("actual_end_time", batch.get("planned_end_time")) - }) - - self.stats["consumptions"] = len(consumptions) - print(f"✅ Calculated {len(consumptions)} consumption records from production") - return consumptions - - def apply_consumptions_to_stock(self, consumptions: List[Dict], stock: List[Dict]): - """Apply consumption calculations to stock data.""" - print("📉 Applying consumptions to stock...") - - # Group consumptions by ingredient - consumption_by_ingredient = defaultdict(float) - for cons in consumptions: - consumption_by_ingredient[cons["ingredient_id"]] += cons["quantity_consumed"] - - # Update stock quantities - for stock_item in stock: - ing_id = stock_item["ingredient_id"] - if ing_id in consumption_by_ingredient: - consumed = consumption_by_ingredient[ing_id] - - # Update quantities - stock_item["current_quantity"] = round(stock_item["current_quantity"] - consumed, 2) - stock_item["available_quantity"] = round(stock_item["available_quantity"] - consumed, 2) - - # Ensure quantities don't go negative - if stock_item["current_quantity"] < 0: - stock_item["current_quantity"] = 0 - if stock_item["available_quantity"] < 0: - stock_item["available_quantity"] = 0 - - print(f"✅ Applied consumptions to {len(stock)} stock items") - - # ======================================================================== - # SALES GENERATION - # ======================================================================== - - def generate_sales_data(self) -> List[Dict]: - """Generate historical sales data aligned with completed batches.""" - print("💰 Generating sales data...") - - batches = self.production_data.get("batches", []) - completed = [b for b in batches if b["status"] == "COMPLETED"] - - sales = [] - sale_id_counter = 1 - - for batch in completed: - product_id = batch["product_id"] - actual_qty = batch.get("actual_quantity", 0) - - # Determine sales from this batch (90-98% of production) - sold_qty = actual_qty * random.uniform(0.90, 0.98) - - # Split into 2-4 sales transactions - num_sales = random.randint(2, 4) - - # Parse batch end time - end_time_str = batch.get("actual_end_time", batch.get("planned_end_time")) - batch_date = parse_timestamp_flexible(end_time_str) - - for i in range(num_sales): - sale_qty = sold_qty / num_sales * random.uniform(0.8, 1.2) - sale_time = batch_date + timedelta(hours=random.uniform(2, 10)) - - # Calculate offset from BASE_TS - offset_delta = sale_time - self.base_ts - - # Handle negative offsets - if offset_delta < timedelta(0): - offset_delta = -offset_delta - offset_str = f"BASE_TS - {abs(offset_delta.days)}d {offset_delta.seconds//3600}h" - else: - offset_str = f"BASE_TS + {offset_delta.days}d {offset_delta.seconds//3600}h" - - sales.append({ - "id": generate_sales_id(), - "tenant_id": self.tenant_id, - "product_id": product_id, - "quantity": round(sale_qty, 2), - "unit_price": round(random.uniform(2.5, 8.5), 2), - "total_amount": round(sale_qty * random.uniform(2.5, 8.5), 2), - "sales_date": offset_str, - "sales_channel": random.choice(["retail", "wholesale", "online"]), - "payment_method": random.choice(["cash", "card", "transfer"]), - "customer_id": "50000000-0000-0000-0000-000000000001", # Generic customer - "created_at": offset_str, - "updated_at": offset_str - }) - sale_id_counter += 1 - - self.stats["sales"] = len(sales) - print(f"✅ Generated {len(sales)} sales records") - return sales - - # ======================================================================== - # FORECASTING GENERATION - # ======================================================================== - - def generate_forecasting_data(self) -> List[Dict]: - """Generate forecasting data with 88-92% accuracy.""" - print("📊 Generating forecasting data...") - - # Get products from inventory - products = [ing for ing in self.inventory_data.get("ingredients", []) - if ing.get("product_type") == "FINISHED_PRODUCT"] - - forecasts = [] - forecast_id_counter = 1 - - # Generate forecasts for next 7 days - for day_offset in range(1, 8): - forecast_date = self.base_ts + timedelta(days=day_offset) - date_str = calculate_timestamp(f"BASE_TS + {day_offset}d") - - for product in products: - # Get historical sales for this product (last 7 days) - historical_sales = self._get_historical_sales(product["id"]) - - # If no historical sales, use a reasonable default based on product type - if not historical_sales: - # Estimate based on product category - product_name = product.get("name", "").lower() - if "baguette" in product_name: - avg_sales = random.uniform(20, 40) - elif "croissant" in product_name: - avg_sales = random.uniform(15, 30) - elif "pan" in product_name or "bread" in product_name: - avg_sales = random.uniform(10, 25) - else: - avg_sales = random.uniform(5, 15) - else: - avg_sales = sum(historical_sales) / len(historical_sales) - - # Generate forecast with 88-92% accuracy (12-8% error) - error_factor = random.uniform(-0.12, 0.12) # ±12% error → ~88% accuracy - predicted = avg_sales * (1 + error_factor) - - # Ensure positive prediction - if predicted < 0: - predicted = avg_sales * 0.8 - - confidence = round(random.uniform(88, 92), 1) - - forecasts.append({ - "id": str(uuid.uuid4()), - "tenant_id": self.tenant_id, - "product_id": product["id"], - "forecast_date": date_str, - "predicted_quantity": round(predicted, 2), - "confidence_percentage": confidence, - "forecast_type": "daily", - "created_at": "BASE_TS", - "updated_at": "BASE_TS", - "notes": f"Forecast accuracy: {confidence}% (seed={RANDOM_SEED})" - }) - forecast_id_counter += 1 - - # Calculate actual accuracy - accuracy = self._calculate_forecasting_accuracy() - self.stats["forecasting_accuracy"] = accuracy - - self.stats["forecasts"] = len(forecasts) - print(f"✅ Generated {len(forecasts)} forecasts with {accuracy}% accuracy") - return forecasts - - def _get_historical_sales(self, product_id: str) -> List[float]: - """Get historical sales for a product (last 7 days).""" - sales = self.sales_data.get("sales_data", []) - - historical = [] - for sale in sales: - if sale.get("product_id") == product_id: - # Parse sale date - sale_date_str = sale.get("sales_date") - if sale_date_str and "BASE_TS" in sale_date_str: - sale_date = parse_timestamp_flexible(sale_date_str) - - # Check if within last 7 days - if 0 <= (sale_date - self.base_ts).days <= 7: - historical.append(sale.get("quantity", 0)) - - return historical - - def _calculate_forecasting_accuracy(self) -> float: - """Calculate historical forecasting accuracy.""" - # This is a simplified calculation - in reality we'd compare actual vs predicted - # For demo purposes, we'll use the target accuracy based on our error factor - return round(random.uniform(88, 92), 1) - - # ======================================================================== - # CROSS-REFERENCE VALIDATION - # ======================================================================== - - def validate_cross_references(self): - """Validate all cross-references between services.""" - print("🔗 Validating cross-references...") - - # Validate production batches product IDs - batches = self.production_data.get("batches", []) - products = {p["id"]: p for p in self.inventory_data.get("ingredients", []) - if p.get("product_type") == "FINISHED_PRODUCT"} - - for batch in batches: - product_id = batch.get("product_id") - if product_id and product_id not in products: - self._add_validation_error(f"Batch {batch['batch_number']} references non-existent product {product_id}") - - # Validate recipe ingredients - recipe_ingredients = self.recipes_data.get("recipe_ingredients", []) - ingredients = {ing["id"]: ing for ing in self.inventory_data.get("ingredients", [])} - - for ri in recipe_ingredients: - ing_id = ri.get("ingredient_id") - if ing_id and ing_id not in ingredients: - self._add_validation_error(f"Recipe ingredient references non-existent ingredient {ing_id}") - - # Validate procurement PO items - pos = self.procurement_data.get("purchase_orders", []) - for po in pos: - for item in po.get("items", []): - inv_product_id = item.get("inventory_product_id") - if inv_product_id and inv_product_id not in self.inventory_data.get("ingredients", []): - self._add_validation_error(f"PO {po['po_number']} references non-existent inventory product {inv_product_id}") - - # Validate sales product IDs - sales = self.sales_data.get("sales_data", []) - for sale in sales: - product_id = sale.get("product_id") - if product_id and product_id not in products: - self._add_validation_error(f"Sales record references non-existent product {product_id}") - - # Validate forecasting product IDs - forecasts = self.forecasting_data.get("forecasts", []) - for forecast in forecasts: - product_id = forecast.get("product_id") - if product_id and product_id not in products: - self._add_validation_error(f"Forecast references non-existent product {product_id}") - - if not self.validation_errors: - print("✅ All cross-references validated successfully") - else: - print(f"❌ Found {len(self.validation_errors)} cross-reference errors") - - # ======================================================================== - # ORCHESTRATOR UPDATE - # ======================================================================== - - def update_orchestrator_results(self): - """Update orchestrator results with actual data.""" - print("🎛️ Updating orchestrator results...") - - # Load orchestrator data - orchestrator_data = self.orchestrator_data - - # Update with actual counts - orchestrator_data["results"] = { - "ingredients_created": self.stats["ingredients"], - "stock_entries_created": self.stats["stock_entries"], - "batches_created": self.stats["batches"], - "sales_created": self.stats["sales"], - "forecasts_created": self.stats["forecasts"], - "consumptions_calculated": self.stats["consumptions"], - "critical_stock_items": self.stats["critical_stock"], - "active_alerts": self.stats["alerts"], - "forecasting_accuracy": self.stats["forecasting_accuracy"], - "cross_reference_errors": len(self.validation_errors), - "cross_reference_warnings": len(self.validation_warnings) - } - - # Add edge case alerts - alerts = [ - { - "alert_type": "OVERDUE_BATCH", - "severity": "high", - "message": "Production should have started 2 hours ago - BATCH-LATE-0001", - "created_at": "BASE_TS" - }, - { - "alert_type": "DELAYED_DELIVERY", - "severity": "high", - "message": "Supplier delivery 4 hours late - PO-LATE-0001", - "created_at": "BASE_TS" - }, - { - "alert_type": "CRITICAL_STOCK", - "severity": "critical", - "message": "Harina T55 below reorder point with NO pending PO", - "created_at": "BASE_TS" - } - ] - - orchestrator_data["alerts"] = alerts - orchestrator_data["completed_at"] = "BASE_TS" - orchestrator_data["status"] = "completed" - - self.orchestrator_data = orchestrator_data - print("✅ Updated orchestrator results with actual data") - - # ======================================================================== - # MAIN EXECUTION - # ======================================================================== - - def generate_all_data(self): - """Generate all demo data.""" - print("🚀 Starting Bakery-IA Demo Data Generation") - print("=" * 60) - - # Step 1: Generate complete inventory - self.generate_complete_inventory() - - # Step 2: Calculate production consumptions - consumptions = self.calculate_production_consumptions() - - # Step 3: Apply consumptions to stock - stock = self.inventory_data.get("stock", []) - self.apply_consumptions_to_stock(consumptions, stock) - self.inventory_data["stock"] = stock - - # Step 4: Generate sales data - sales_data = self.generate_sales_data() - self.sales_data["sales_data"] = sales_data - - # Step 5: Generate forecasting data - forecasts = self.generate_forecasting_data() - self.forecasting_data["forecasts"] = forecasts - - # Step 6: Validate cross-references - self.validate_cross_references() - - # Step 7: Update orchestrator results - self.update_orchestrator_results() - - # Step 8: Save all data - self.save_all_data() - - # Step 9: Generate report - self.generate_report() - - print("\n🎉 Demo Data Generation Complete!") - print(f"📊 Generated {sum(self.stats.values())} total records") - print(f"✅ Validation: {len(self.validation_errors)} errors, {len(self.validation_warnings)} warnings") - - def save_all_data(self): - """Save all generated data to JSON files.""" - print("💾 Saving generated data...") - - # Save inventory - save_json("03-inventory.json", self.inventory_data) - - # Save production (no changes needed, but save for completeness) - save_json("06-production.json", self.production_data) - - # Save procurement (no changes needed) - save_json("07-procurement.json", self.procurement_data) - - # Save sales - save_json("09-sales.json", self.sales_data) - - # Save forecasting - save_json("10-forecasting.json", self.forecasting_data) - - # Save orchestrator - save_json("11-orchestrator.json", self.orchestrator_data) - - print("✅ All data saved to JSON files") - - def generate_report(self): - """Generate comprehensive report.""" - print("📋 Generating report...") - - report = f"""# Bakery-IA Demo Data Generation Report - -## Executive Summary - -**Generation Date**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} -**Tier**: Professional - Panadería Artesana Madrid -**BASE_TS**: {BASE_TS.strftime('%Y-%m-%dT%H:%M:%SZ')} -**Random Seed**: {RANDOM_SEED} - -## Generation Statistics - -### Data Generated -- **Ingredients**: {self.stats['ingredients']} -- **Stock Entries**: {self.stats['stock_entries']} -- **Production Batches**: {self.stats['batches']} -- **Sales Records**: {self.stats['sales']} -- **Forecasts**: {self.stats['forecasts']} -- **Consumption Records**: {self.stats['consumptions']} - -### Alerts & Critical Items -- **Critical Stock Items**: {self.stats['critical_stock']} -- **Active Alerts**: {self.stats['alerts']} -- **Forecasting Accuracy**: {self.stats['forecasting_accuracy']}% - -### Validation Results -- **Cross-Reference Errors**: {len(self.validation_errors)} -- **Cross-Reference Warnings**: {len(self.validation_warnings)} - -## Changes Made - -""" - - # Add changes - if self.changes: - report += "### Changes\n\n" - for change in self.changes: - report += f"- {change}\n" - else: - report += "### Changes\n\nNo changes made (data already complete)\n" - - # Add validation issues - if self.validation_errors or self.validation_warnings: - report += "\n## Validation Issues\n\n" - - if self.validation_errors: - report += "### Errors\n\n" - for error in self.validation_errors: - report += f"- ❌ {error}\n" - - if self.validation_warnings: - report += "### Warnings\n\n" - for warning in self.validation_warnings: - report += f"- ⚠️ {warning}\n" - else: - report += "\n## Validation Issues\n\n✅ No validation issues found\n" - - # Add edge cases - report += f""" -## Edge Cases Maintained - -### Inventory Edge Cases -- **Harina T55**: 80kg < 150kg reorder point, NO pending PO → RED alert -- **Mantequilla**: 25kg < 40kg reorder point, has PO-2025-006 → WARNING -- **Levadura Fresca**: 8kg < 10kg reorder point, has PO-2025-004 → WARNING - -### Production Edge Cases -- **OVERDUE BATCH**: BATCH-LATE-0001 (Baguette, planned start: BASE_TS - 2h) -- **IN_PROGRESS BATCH**: BATCH-INPROGRESS-0001 (Croissant, started: BASE_TS - 1h45m) -- **UPCOMING BATCH**: BATCH-UPCOMING-0001 (Pan Integral, planned: BASE_TS + 1h30m) -- **QUARANTINED BATCH**: batch 000000000004 (Napolitana Chocolate, quality failed) - -### Procurement Edge Cases -- **LATE DELIVERY**: PO-LATE-0001 (expected: BASE_TS - 4h, status: pending_approval) -- **URGENT PO**: PO-2025-004 (status: confirmed, delivery late) - -## Cross-Reference Validation - -### Validated References -- ✅ Production batches → Inventory products -- ✅ Recipe ingredients → Inventory ingredients -- ✅ Procurement PO items → Inventory products -- ✅ Sales records → Inventory products -- ✅ Forecasting → Inventory products - -## KPIs Dashboard - -```json -{{ - "production_fulfillment": 87, - "critical_stock_count": {self.stats['critical_stock']}, - "open_alerts": {self.stats['alerts']}, - "forecasting_accuracy": {self.stats['forecasting_accuracy']}, - "batches_today": {{ - "overdue": 1, - "in_progress": 1, - "upcoming": 2, - "completed": 0 - }} -}} -``` - -## Technical Details - -### Deterministic Generation -- **Random Seed**: {RANDOM_SEED} -- **Variations**: ±10-20% in quantities, ±5-10% in prices -- **Batch Numbers**: Format `SKU-YYYYMMDD-NNN` -- **Timestamps**: Relative to BASE_TS with offsets - -### Data Quality -- **Completeness**: All ingredients have stock entries -- **Consistency**: Production consumptions aligned with inventory -- **Accuracy**: Forecasting accuracy {self.stats['forecasting_accuracy']}% -- **Validation**: {len(self.validation_errors)} errors, {len(self.validation_warnings)} warnings - -## Files Updated - -- `shared/demo/fixtures/professional/03-inventory.json` -- `shared/demo/fixtures/professional/06-production.json` -- `shared/demo/fixtures/professional/07-procurement.json` -- `shared/demo/fixtures/professional/09-sales.json` -- `shared/demo/fixtures/professional/10-forecasting.json` -- `shared/demo/fixtures/professional/11-orchestrator.json` - -## Conclusion - -✅ **Demo data generation completed successfully** -- All cross-references validated -- Edge cases maintained -- Forecasting accuracy: {self.stats['forecasting_accuracy']}% -- Critical stock items: {self.stats['critical_stock']} -- Active alerts: {self.stats['alerts']} - -**Status**: Ready for demo deployment 🎉 -""" - - # Save report - report_path = BASE_DIR / "DEMO_DATA_GENERATION_REPORT.md" - with open(report_path, 'w', encoding='utf-8') as f: - f.write(report) - - print(f"✅ Report saved to {report_path}") - -# ============================================================================ -# MAIN EXECUTION -# ============================================================================ - -def main(): - """Main execution function.""" - print("🚀 Starting Improved Bakery-IA Demo Data Generation") - print("=" * 60) - - # Initialize generator - generator = DemoDataGenerator() - - # Generate all data - generator.generate_all_data() - - print("\n🎉 All tasks completed successfully!") - print("📋 Summary:") - print(f" • Generated complete inventory with {generator.stats['ingredients']} ingredients") - print(f" • Calculated {generator.stats['consumptions']} production consumptions") - print(f" • Generated {generator.stats['sales']} sales records") - print(f" • Generated {generator.stats['forecasts']} forecasts with {generator.stats['forecasting_accuracy']}% accuracy") - print(f" • Validated all cross-references") - print(f" • Updated orchestrator results") - print(f" • Validation: {len(generator.validation_errors)} errors, {len(generator.validation_warnings)} warnings") - - if generator.validation_errors: - print("\n⚠️ Please review validation errors above") - return 1 - else: - print("\n✅ All data validated successfully - ready for deployment!") - return 0 - -if __name__ == "__main__": - exit(main()) \ No newline at end of file diff --git a/scripts/generate_service_token.py b/scripts/generate_service_token.py deleted file mode 100755 index 9955c24a..00000000 --- a/scripts/generate_service_token.py +++ /dev/null @@ -1,244 +0,0 @@ -#!/usr/bin/env python3 -""" -Generate Service-to-Service Authentication Token - -This script generates JWT tokens for service-to-service communication -in the Bakery-IA tenant deletion system. - -Usage: - python scripts/generate_service_token.py [--days DAYS] - -Examples: - # Generate token for orchestrator (1 year expiration) - python scripts/generate_service_token.py tenant-deletion-orchestrator - - # Generate token for specific service with custom expiration - python scripts/generate_service_token.py auth-service --days 90 - - # Generate tokens for all services - python scripts/generate_service_token.py --all -""" - -import sys -import os -import argparse -from datetime import timedelta -from pathlib import Path - -# Add project root to path -project_root = Path(__file__).parent.parent -sys.path.insert(0, str(project_root)) - -from shared.auth.jwt_handler import JWTHandler - -# Get JWT secret from environment (same as services use) -JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "your-secret-key-change-in-production-min-32-chars") - -# Service names used in the system -SERVICES = [ - "tenant-deletion-orchestrator", - "auth-service", - "tenant-service", - "orders-service", - "inventory-service", - "recipes-service", - "sales-service", - "production-service", - "suppliers-service", - "pos-service", - "external-service", - "forecasting-service", - "training-service", - "alert-processor-service", - "notification-service" -] - - -def generate_token(service_name: str, days: int = 365) -> str: - """ - Generate a service token - - Args: - service_name: Name of the service - days: Token expiration in days (default: 365) - - Returns: - JWT service token - """ - jwt_handler = JWTHandler( - secret_key=JWT_SECRET_KEY, - algorithm="HS256" - ) - - token = jwt_handler.create_service_token( - service_name=service_name, - expires_delta=timedelta(days=days) - ) - - return token - - -def verify_token(token: str) -> dict: - """ - Verify a service token and return its payload - - Args: - token: JWT token to verify - - Returns: - Token payload dictionary - """ - jwt_handler = JWTHandler( - secret_key=JWT_SECRET_KEY, - algorithm="HS256" - ) - - payload = jwt_handler.verify_token(token) - if not payload: - raise ValueError("Invalid or expired token") - - return payload - - -def main(): - parser = argparse.ArgumentParser( - description="Generate service-to-service authentication tokens", - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=""" -Examples: - # Generate token for orchestrator - %(prog)s tenant-deletion-orchestrator - - # Generate token with custom expiration - %(prog)s auth-service --days 90 - - # Generate tokens for all services - %(prog)s --all - - # Verify a token - %(prog)s --verify - """ - ) - - parser.add_argument( - "service_name", - nargs="?", - help="Name of the service (e.g., 'tenant-deletion-orchestrator')" - ) - - parser.add_argument( - "--days", - type=int, - default=365, - help="Token expiration in days (default: 365)" - ) - - parser.add_argument( - "--all", - action="store_true", - help="Generate tokens for all services" - ) - - parser.add_argument( - "--verify", - metavar="TOKEN", - help="Verify a token and show its payload" - ) - - parser.add_argument( - "--list-services", - action="store_true", - help="List all available service names" - ) - - args = parser.parse_args() - - # List services - if args.list_services: - print("\nAvailable Services:") - print("=" * 50) - for service in SERVICES: - print(f" - {service}") - print() - return 0 - - # Verify token - if args.verify: - try: - payload = verify_token(args.verify) - print("\n✓ Token is valid!") - print("=" * 50) - print(f"Service Name: {payload.get('service')}") - print(f"Type: {payload.get('type')}") - print(f"Is Service: {payload.get('is_service')}") - print(f"Role: {payload.get('role')}") - print(f"Issued At: {payload.get('iat')}") - print(f"Expires At: {payload.get('exp')}") - print("=" * 50) - print() - return 0 - except Exception as e: - print(f"\n✗ Token verification failed: {e}\n") - return 1 - - # Generate for all services - if args.all: - print(f"\nGenerating service tokens (expires in {args.days} days)...") - print("=" * 80) - - for service in SERVICES: - try: - token = generate_token(service, args.days) - print(f"\n{service}:") - print(f" export {service.upper().replace('-', '_')}_TOKEN='{token}'") - except Exception as e: - print(f"\n✗ Failed to generate token for {service}: {e}") - - print("\n" + "=" * 80) - print("\nℹ Copy the export statements above to set environment variables") - print("ℹ Or save them to a .env file for your services\n") - return 0 - - # Generate for single service - if not args.service_name: - parser.print_help() - return 1 - - try: - print(f"\nGenerating service token for: {args.service_name}") - print(f"Expiration: {args.days} days") - print("=" * 80) - - token = generate_token(args.service_name, args.days) - - print("\n✓ Token generated successfully!\n") - print("Token:") - print(f" {token}") - print() - print("Environment Variable:") - env_var = args.service_name.upper().replace('-', '_') + '_TOKEN' - print(f" export {env_var}='{token}'") - print() - print("Usage in Code:") - print(f" headers = {{'Authorization': f'Bearer {{os.getenv(\"{env_var}\")}}'}}") - print() - print("Test with curl:") - print(f" curl -H 'Authorization: Bearer {token}' https://localhost/api/v1/...") - print() - print("=" * 80) - print() - - # Verify the token we just created - print("Verifying token...") - payload = verify_token(token) - print("✓ Token is valid and verified!\n") - - return 0 - - except Exception as e: - print(f"\n✗ Error: {e}\n") - return 1 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/migrate_json_to_base_ts.py b/scripts/migrate_json_to_base_ts.py deleted file mode 100644 index e66c6621..00000000 --- a/scripts/migrate_json_to_base_ts.py +++ /dev/null @@ -1,283 +0,0 @@ -#!/usr/bin/env python3 -""" -Migrate all demo JSON files from offset_days/ISO timestamps to BASE_TS markers. -This script performs a one-time migration to align with the new architecture. -""" - -import json -import sys -from pathlib import Path -from datetime import datetime, timezone -from typing import Any, Dict - -# Base reference date used in current JSON files -BASE_REFERENCE_ISO = "2025-01-15T06:00:00Z" -BASE_REFERENCE = datetime.fromisoformat(BASE_REFERENCE_ISO.replace('Z', '+00:00')) - -# Date fields to transform by entity type -DATE_FIELDS_MAP = { - 'purchase_orders': [ - 'order_date', 'required_delivery_date', 'estimated_delivery_date', - 'expected_delivery_date', 'sent_to_supplier_at', 'supplier_confirmation_date', - 'created_at', 'updated_at' - ], - 'batches': [ - 'planned_start_time', 'planned_end_time', 'actual_start_time', - 'actual_end_time', 'completed_at', 'created_at', 'updated_at' - ], - 'equipment': [ - 'install_date', 'last_maintenance_date', 'next_maintenance_date', - 'created_at', 'updated_at' - ], - 'ingredients': ['created_at', 'updated_at'], - 'stock_batches': [ - 'received_date', 'expiration_date', 'best_before_date', - 'created_at', 'updated_at' - ], - 'customers': ['last_order_date', 'created_at', 'updated_at'], - 'orders': [ - 'order_date', 'delivery_date', 'promised_date', - 'completed_at', 'created_at', 'updated_at' - ], - 'completed_orders': [ - 'order_date', 'delivery_date', 'promised_date', - 'completed_at', 'created_at', 'updated_at' - ], - 'forecasts': ['forecast_date', 'created_at', 'updated_at'], - 'prediction_batches': ['prediction_date', 'created_at', 'updated_at'], - 'sales_data': ['created_at', 'updated_at'], - 'quality_controls': ['created_at', 'updated_at'], - 'quality_alerts': ['created_at', 'updated_at'], - 'customer_orders': [ - 'order_date', 'delivery_date', 'promised_date', - 'completed_at', 'created_at', 'updated_at' - ], - 'order_items': ['created_at', 'updated_at'], - 'procurement_requirements': ['created_at', 'updated_at'], - 'replenishment_plans': ['created_at', 'updated_at'], - 'production_schedules': ['schedule_date', 'created_at', 'updated_at'], - 'users': ['created_at', 'updated_at'], - 'stock': ['expiration_date', 'received_date', 'created_at', 'updated_at'], - 'recipes': ['created_at', 'updated_at'], - 'recipe_ingredients': ['created_at', 'updated_at'], - 'suppliers': ['created_at', 'updated_at'], - 'production_batches': ['start_time', 'end_time', 'created_at', 'updated_at'], - 'purchase_order_items': ['created_at', 'updated_at'], - # Enterprise children files - 'local_inventory': ['expiration_date', 'received_date', 'created_at', 'updated_at'], - 'local_sales': ['created_at', 'updated_at'], - 'local_orders': ['order_date', 'delivery_date', 'created_at', 'updated_at'], - 'local_production_batches': [ - 'planned_start_time', 'planned_end_time', 'actual_start_time', - 'actual_end_time', 'created_at', 'updated_at' - ], - 'local_forecasts': ['forecast_date', 'created_at', 'updated_at'] -} - - -def calculate_offset_from_base(iso_timestamp: str) -> str: - """ - Calculate BASE_TS offset from an ISO timestamp. - - Args: - iso_timestamp: ISO 8601 timestamp string - - Returns: - BASE_TS marker string (e.g., "BASE_TS + 2d 3h") - """ - try: - target_time = datetime.fromisoformat(iso_timestamp.replace('Z', '+00:00')) - except (ValueError, AttributeError): - return None - - # Calculate offset from BASE_REFERENCE - offset = target_time - BASE_REFERENCE - total_seconds = int(offset.total_seconds()) - - if total_seconds == 0: - return "BASE_TS" - - # Convert to days, hours, minutes - days = offset.days - remaining_seconds = total_seconds - (days * 86400) - hours = remaining_seconds // 3600 - minutes = (remaining_seconds % 3600) // 60 - - # Build BASE_TS expression - parts = [] - if days != 0: - parts.append(f"{abs(days)}d") - if hours != 0: - parts.append(f"{abs(hours)}h") - if minutes != 0: - parts.append(f"{abs(minutes)}m") - - if not parts: - return "BASE_TS" - - operator = "+" if total_seconds > 0 else "-" - return f"BASE_TS {operator} {' '.join(parts)}" - - -def migrate_date_field(value: Any, field_name: str) -> Any: - """ - Migrate a single date field to BASE_TS format. - - Args: - value: Field value (can be ISO string, offset_days dict, or None) - field_name: Name of the field being migrated - - Returns: - BASE_TS marker string or original value (if already BASE_TS or None) - """ - if value is None: - return None - - # Already a BASE_TS marker - keep as-is - if isinstance(value, str) and value.startswith("BASE_TS"): - return value - - # Handle ISO timestamp strings - if isinstance(value, str) and ('T' in value or 'Z' in value): - return calculate_offset_from_base(value) - - # Handle offset_days dictionary format (from inventory stock) - if isinstance(value, dict) and 'offset_days' in value: - days = value.get('offset_days', 0) - hour = value.get('hour', 0) - minute = value.get('minute', 0) - - parts = [] - if days != 0: - parts.append(f"{abs(days)}d") - if hour != 0: - parts.append(f"{abs(hour)}h") - if minute != 0: - parts.append(f"{abs(minute)}m") - - if not parts: - return "BASE_TS" - - operator = "+" if days >= 0 else "-" - return f"BASE_TS {operator} {' '.join(parts)}" - - return None - - -def migrate_entity(entity: Dict[str, Any], date_fields: list) -> Dict[str, Any]: - """ - Migrate all date fields in an entity to BASE_TS format. - - Also removes *_offset_days fields as they're now redundant. - - Args: - entity: Entity dictionary - date_fields: List of date field names to migrate - - Returns: - Migrated entity dictionary - """ - migrated = entity.copy() - - # Remove offset_days fields and migrate their values - offset_fields_to_remove = [] - for key in list(migrated.keys()): - if key.endswith('_offset_days'): - # Extract base field name - base_field = key.replace('_offset_days', '') - - # Calculate BASE_TS marker - offset_days = migrated[key] - if offset_days == 0: - migrated[base_field] = "BASE_TS" - else: - operator = "+" if offset_days > 0 else "-" - migrated[base_field] = f"BASE_TS {operator} {abs(offset_days)}d" - - offset_fields_to_remove.append(key) - - # Remove offset_days fields - for key in offset_fields_to_remove: - del migrated[key] - - # Migrate ISO timestamp fields - for field in date_fields: - if field in migrated: - migrated[field] = migrate_date_field(migrated[field], field) - - return migrated - - -def migrate_json_file(file_path: Path) -> bool: - """ - Migrate a single JSON file to BASE_TS format. - - Args: - file_path: Path to JSON file - - Returns: - True if file was modified, False otherwise - """ - print(f"\n📄 Processing: {file_path.relative_to(file_path.parents[3])}") - - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - except Exception as e: - print(f" ❌ Failed to load: {e}") - return False - - modified = False - - # Migrate each entity type - for entity_type, date_fields in DATE_FIELDS_MAP.items(): - if entity_type in data: - original_count = len(data[entity_type]) - data[entity_type] = [ - migrate_entity(entity, date_fields) - for entity in data[entity_type] - ] - if original_count > 0: - print(f" ✅ Migrated {original_count} {entity_type}") - modified = True - - if modified: - # Write back with pretty formatting - with open(file_path, 'w', encoding='utf-8') as f: - json.dump(data, f, indent=2, ensure_ascii=False) - print(f" 💾 File updated successfully") - - return modified - - -def main(): - """Main migration function""" - # Find all JSON files in demo fixtures - root_dir = Path(__file__).parent.parent - fixtures_dir = root_dir / "shared" / "demo" / "fixtures" - - if not fixtures_dir.exists(): - print(f"❌ Fixtures directory not found: {fixtures_dir}") - return 1 - - # Find all JSON files - json_files = list(fixtures_dir.rglob("*.json")) - - if not json_files: - print(f"❌ No JSON files found in {fixtures_dir}") - return 1 - - print(f"🔍 Found {len(json_files)} JSON files to migrate") - - # Migrate each file - total_modified = 0 - for json_file in sorted(json_files): - if migrate_json_file(json_file): - total_modified += 1 - - print(f"\n✅ Migration complete: {total_modified}/{len(json_files)} files modified") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/quick_test_deletion.sh b/scripts/quick_test_deletion.sh deleted file mode 100755 index 0a516e9c..00000000 --- a/scripts/quick_test_deletion.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/bin/bash -# Quick test script for deletion endpoints via localhost (port-forwarded or ingress) -# This tests with the real Bakery-IA demo tenant - -set -e - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' - -# Demo tenant from the system -TENANT_ID="dbc2128a-7539-470c-94b9-c1e37031bd77" -DEMO_SESSION_ID="demo_8rkT9JjXWFuVmdqT798Nyg" - -# Base URL (through ingress or port-forward) -BASE_URL="${BASE_URL:-https://localhost}" - -echo -e "${BLUE}Testing Deletion System with Real Services${NC}" -echo -e "${BLUE}===========================================${NC}" -echo "" -echo -e "Tenant ID: ${YELLOW}$TENANT_ID${NC}" -echo -e "Base URL: ${YELLOW}$BASE_URL${NC}" -echo "" - -# Test function -test_service() { - local service_name=$1 - local endpoint=$2 - - echo -n "Testing $service_name... " - - # Try to access the deletion preview endpoint - response=$(curl -k -s -w "\n%{http_code}" \ - -H "X-Demo-Session-Id: $DEMO_SESSION_ID" \ - -H "X-Tenant-ID: $TENANT_ID" \ - "$BASE_URL$endpoint/tenant/$TENANT_ID/deletion-preview" 2>&1) - - http_code=$(echo "$response" | tail -1) - body=$(echo "$response" | sed '$d') - - if [ "$http_code" = "200" ]; then - # Try to parse total records - total=$(echo "$body" | grep -o '"total_records":[0-9]*' | cut -d':' -f2 || echo "?") - echo -e "${GREEN}✓${NC} (HTTP $http_code, Records: $total)" - elif [ "$http_code" = "401" ] || [ "$http_code" = "403" ]; then - echo -e "${YELLOW}⚠${NC} (HTTP $http_code - Auth required)" - elif [ "$http_code" = "404" ]; then - echo -e "${RED}✗${NC} (HTTP $http_code - Endpoint not found)" - else - echo -e "${RED}✗${NC} (HTTP $http_code)" - fi -} - -# Test all services -echo "Testing deletion preview endpoints:" -echo "" - -test_service "Orders" "/api/v1/orders" -test_service "Inventory" "/api/v1/inventory" -test_service "Recipes" "/api/v1/recipes" -test_service "Sales" "/api/v1/sales" -test_service "Production" "/api/v1/production" -test_service "Suppliers" "/api/v1/suppliers" -test_service "POS" "/api/v1/pos" -test_service "External" "/api/v1/external" -test_service "Forecasting" "/api/v1/forecasting" -test_service "Training" "/api/v1/training" -test_service "Alert Processor" "/api/v1/alerts" -test_service "Notification" "/api/v1/notifications" - -echo "" -echo -e "${BLUE}Test completed!${NC}" -echo "" -echo -e "${YELLOW}Note:${NC} 401/403 responses are expected - deletion endpoints require service tokens" -echo -e "${YELLOW}Note:${NC} To test with proper auth, set up service-to-service authentication" diff --git a/scripts/register_audit_routers.sh b/scripts/register_audit_routers.sh deleted file mode 100644 index 5bcecd1e..00000000 --- a/scripts/register_audit_routers.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash -# Script to register audit routers in all service main.py files - -set -e - -BASE_DIR="/Users/urtzialfaro/Documents/bakery-ia/services" - -echo "Registering audit routers in service main.py files..." - -# Function to add audit import and router registration -add_audit_to_service() { - local service=$1 - local main_file="$BASE_DIR/$service/app/main.py" - - if [ ! -f "$main_file" ]; then - echo "⚠️ $service: main.py not found, skipping" - return - fi - - # Check if audit is already imported - if grep -q "import.*audit" "$main_file"; then - echo "✓ $service: audit already imported" - else - echo "⚠️ $service: needs manual import addition" - fi - - # Check if audit router is already registered - if grep -q "service.add_router(audit.router)" "$main_file"; then - echo "✓ $service: audit router already registered" - else - echo "⚠️ $service: needs manual router registration" - fi -} - -# Process each service -for service in recipes suppliers pos training notification external forecasting; do - add_audit_to_service "$service" -done - -echo "" -echo "Done! Please check warnings above for services needing manual updates." diff --git a/scripts/test/demo_determinism.py b/scripts/test/demo_determinism.py deleted file mode 100644 index 2e732b92..00000000 --- a/scripts/test/demo_determinism.py +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env python3 -""" -Test deterministic cloning by creating multiple sessions and comparing data hashes. -""" -import asyncio -import hashlib -import json -from typing import List, Dict -import httpx - -DEMO_API_URL = "http://localhost:8018" -INTERNAL_API_KEY = "test-internal-key" - -async def create_demo_session(tier: str = "professional") -> dict: - """Create a demo session""" - async with httpx.AsyncClient() as client: - response = await client.post( - f"{DEMO_API_URL}/api/demo/sessions", - json={"demo_account_type": tier} - ) - return response.json() - -async def get_all_data_from_service( - service_url: str, - tenant_id: str -) -> dict: - """Fetch all data for a tenant from a service""" - async with httpx.AsyncClient() as client: - response = await client.get( - f"{service_url}/internal/demo/export/{tenant_id}", - headers={"X-Internal-API-Key": INTERNAL_API_KEY} - ) - return response.json() - -def calculate_data_hash(data: dict) -> str: - """ - Calculate SHA-256 hash of data, excluding audit timestamps. - """ - # Remove non-deterministic fields - clean_data = remove_audit_fields(data) - - # Sort keys for consistency - json_str = json.dumps(clean_data, sort_keys=True) - - return hashlib.sha256(json_str.encode()).hexdigest() - -def remove_audit_fields(data: dict) -> dict: - """Remove created_at, updated_at fields recursively""" - if isinstance(data, dict): - return { - k: remove_audit_fields(v) - for k, v in data.items() - if k not in ["created_at", "updated_at", "id"] # IDs are UUIDs - } - elif isinstance(data, list): - return [remove_audit_fields(item) for item in data] - else: - return data - -async def test_determinism(tier: str = "professional", iterations: int = 10): - """ - Test that cloning is deterministic across multiple sessions. - """ - print(f"Testing determinism for {tier} tier ({iterations} iterations)...") - - services = [ - ("inventory", "http://inventory-service:8002"), - ("production", "http://production-service:8003"), - ("recipes", "http://recipes-service:8004"), - ] - - hashes_by_service = {svc[0]: [] for svc in services} - - for i in range(iterations): - # Create session - session = await create_demo_session(tier) - tenant_id = session["virtual_tenant_id"] - - # Get data from each service - for service_name, service_url in services: - data = await get_all_data_from_service(service_url, tenant_id) - data_hash = calculate_data_hash(data) - hashes_by_service[service_name].append(data_hash) - - # Cleanup - async with httpx.AsyncClient() as client: - await client.delete(f"{DEMO_API_URL}/api/demo/sessions/{session['session_id']}") - - if (i + 1) % 10 == 0: - print(f" Completed {i + 1}/{iterations} iterations") - - # Check consistency - all_consistent = True - for service_name, hashes in hashes_by_service.items(): - unique_hashes = set(hashes) - if len(unique_hashes) == 1: - print(f"✅ {service_name}: All {iterations} hashes identical") - else: - print(f"❌ {service_name}: {len(unique_hashes)} different hashes found!") - all_consistent = False - - if all_consistent: - print("\n✅ DETERMINISM TEST PASSED") - return 0 - else: - print("\n❌ DETERMINISM TEST FAILED") - return 1 - -if __name__ == "__main__": - exit_code = asyncio.run(test_determinism()) - exit(exit_code) \ No newline at end of file diff --git a/scripts/test_deletion_endpoints.sh b/scripts/test_deletion_endpoints.sh deleted file mode 100755 index 17cc2882..00000000 --- a/scripts/test_deletion_endpoints.sh +++ /dev/null @@ -1,140 +0,0 @@ -#!/bin/bash -# Quick script to test all deletion endpoints -# Usage: ./test_deletion_endpoints.sh - -set -e - -TENANT_ID=${1:-"test-tenant-123"} -BASE_URL=${BASE_URL:-"http://localhost:8000"} -TOKEN=${AUTH_TOKEN:-"test-token"} - -echo "================================" -echo "Testing Deletion Endpoints" -echo "Tenant ID: $TENANT_ID" -echo "Base URL: $BASE_URL" -echo "================================" -echo "" - -# Colors -GREEN='\033[0;32m' -RED='\033[0;31m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Function to test endpoint -test_endpoint() { - local service=$1 - local method=$2 - local path=$3 - local expected_status=${4:-200} - - echo -n "Testing $service ($method $path)... " - - response=$(curl -s -w "\n%{http_code}" \ - -X $method \ - -H "Authorization: Bearer $TOKEN" \ - -H "X-Internal-Service: test-script" \ - "$BASE_URL/api/v1/$path" 2>&1) - - status_code=$(echo "$response" | tail -n1) - body=$(echo "$response" | head -n-1) - - if [ "$status_code" == "$expected_status" ] || [ "$status_code" == "404" ]; then - if [ "$status_code" == "404" ]; then - echo -e "${YELLOW}NOT IMPLEMENTED${NC} (404)" - else - echo -e "${GREEN}✓ PASSED${NC} ($status_code)" - if [ "$method" == "GET" ]; then - # Show preview counts - total=$(echo "$body" | jq -r '.total_items // 0' 2>/dev/null || echo "N/A") - if [ "$total" != "N/A" ]; then - echo " → Preview: $total items would be deleted" - fi - elif [ "$method" == "DELETE" ]; then - # Show deletion summary - deleted=$(echo "$body" | jq -r '.summary.total_deleted // 0' 2>/dev/null || echo "N/A") - if [ "$deleted" != "N/A" ]; then - echo " → Deleted: $deleted items" - fi - fi - fi - else - echo -e "${RED}✗ FAILED${NC} ($status_code)" - echo " Response: $body" - fi -} - -echo "=== COMPLETED SERVICES ===" -echo "" - -echo "1. Tenant Service:" -test_endpoint "tenant" "GET" "tenants/$TENANT_ID" -test_endpoint "tenant" "DELETE" "tenants/$TENANT_ID" -echo "" - -echo "2. Orders Service:" -test_endpoint "orders" "GET" "orders/tenant/$TENANT_ID/deletion-preview" -test_endpoint "orders" "DELETE" "orders/tenant/$TENANT_ID" -echo "" - -echo "3. Inventory Service:" -test_endpoint "inventory" "GET" "inventory/tenant/$TENANT_ID/deletion-preview" -test_endpoint "inventory" "DELETE" "inventory/tenant/$TENANT_ID" -echo "" - -echo "4. Recipes Service:" -test_endpoint "recipes" "GET" "recipes/tenant/$TENANT_ID/deletion-preview" -test_endpoint "recipes" "DELETE" "recipes/tenant/$TENANT_ID" -echo "" - -echo "5. Sales Service:" -test_endpoint "sales" "GET" "sales/tenant/$TENANT_ID/deletion-preview" -test_endpoint "sales" "DELETE" "sales/tenant/$TENANT_ID" -echo "" - -echo "6. Production Service:" -test_endpoint "production" "GET" "production/tenant/$TENANT_ID/deletion-preview" -test_endpoint "production" "DELETE" "production/tenant/$TENANT_ID" -echo "" - -echo "7. Suppliers Service:" -test_endpoint "suppliers" "GET" "suppliers/tenant/$TENANT_ID/deletion-preview" -test_endpoint "suppliers" "DELETE" "suppliers/tenant/$TENANT_ID" -echo "" - -echo "=== PENDING SERVICES ===" -echo "" - -echo "8. POS Service:" -test_endpoint "pos" "GET" "pos/tenant/$TENANT_ID/deletion-preview" -test_endpoint "pos" "DELETE" "pos/tenant/$TENANT_ID" -echo "" - -echo "9. External Service:" -test_endpoint "external" "GET" "external/tenant/$TENANT_ID/deletion-preview" -test_endpoint "external" "DELETE" "external/tenant/$TENANT_ID" -echo "" - -echo "10. Alert Processor Service:" -test_endpoint "alert_processor" "GET" "alerts/tenant/$TENANT_ID/deletion-preview" -test_endpoint "alert_processor" "DELETE" "alerts/tenant/$TENANT_ID" -echo "" - -echo "11. Forecasting Service:" -test_endpoint "forecasting" "GET" "forecasts/tenant/$TENANT_ID/deletion-preview" -test_endpoint "forecasting" "DELETE" "forecasts/tenant/$TENANT_ID" -echo "" - -echo "12. Training Service:" -test_endpoint "training" "GET" "models/tenant/$TENANT_ID/deletion-preview" -test_endpoint "training" "DELETE" "models/tenant/$TENANT_ID" -echo "" - -echo "13. Notification Service:" -test_endpoint "notification" "GET" "notifications/tenant/$TENANT_ID/deletion-preview" -test_endpoint "notification" "DELETE" "notifications/tenant/$TENANT_ID" -echo "" - -echo "================================" -echo "Testing Complete!" -echo "================================" diff --git a/scripts/test_deletion_system.sh b/scripts/test_deletion_system.sh deleted file mode 100755 index db8ffc9f..00000000 --- a/scripts/test_deletion_system.sh +++ /dev/null @@ -1,225 +0,0 @@ -#!/bin/bash -# ================================================================ -# Tenant Deletion System - Integration Test Script -# ================================================================ -# Tests all 12 services' deletion endpoints -# Usage: ./scripts/test_deletion_system.sh [tenant_id] - -set -e - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Configuration -TENANT_ID="${1:-dbc2128a-7539-470c-94b9-c1e37031bd77}" # Default demo tenant -SERVICE_TOKEN="${SERVICE_TOKEN:-demo_service_token}" - -# Service URLs (update these based on your environment) -ORDERS_URL="${ORDERS_URL:-http://localhost:8000/api/v1/orders}" -INVENTORY_URL="${INVENTORY_URL:-http://localhost:8001/api/v1/inventory}" -RECIPES_URL="${RECIPES_URL:-http://localhost:8002/api/v1/recipes}" -SALES_URL="${SALES_URL:-http://localhost:8003/api/v1/sales}" -PRODUCTION_URL="${PRODUCTION_URL:-http://localhost:8004/api/v1/production}" -SUPPLIERS_URL="${SUPPLIERS_URL:-http://localhost:8005/api/v1/suppliers}" -POS_URL="${POS_URL:-http://localhost:8006/api/v1/pos}" -EXTERNAL_URL="${EXTERNAL_URL:-http://localhost:8007/api/v1/external}" -FORECASTING_URL="${FORECASTING_URL:-http://localhost:8008/api/v1/forecasting}" -TRAINING_URL="${TRAINING_URL:-http://localhost:8009/api/v1/training}" -ALERT_PROCESSOR_URL="${ALERT_PROCESSOR_URL:-http://localhost:8000/api/v1/alerts}" -NOTIFICATION_URL="${NOTIFICATION_URL:-http://localhost:8011/api/v1/notifications}" - -# Test results -TOTAL_TESTS=0 -PASSED_TESTS=0 -FAILED_TESTS=0 -declare -a FAILED_SERVICES - -# Helper functions -print_header() { - echo -e "${BLUE}================================================${NC}" - echo -e "${BLUE}$1${NC}" - echo -e "${BLUE}================================================${NC}" -} - -print_success() { - echo -e "${GREEN}✓${NC} $1" -} - -print_error() { - echo -e "${RED}✗${NC} $1" -} - -print_warning() { - echo -e "${YELLOW}⚠${NC} $1" -} - -print_info() { - echo -e "${BLUE}ℹ${NC} $1" -} - -# Test individual service deletion preview -test_service_preview() { - local service_name=$1 - local service_url=$2 - local endpoint_path=$3 - - TOTAL_TESTS=$((TOTAL_TESTS + 1)) - - echo "" - print_info "Testing $service_name service..." - - local full_url="${service_url}${endpoint_path}/tenant/${TENANT_ID}/deletion-preview" - - # Make request - response=$(curl -k -s -w "\nHTTP_STATUS:%{http_code}" \ - -H "Authorization: Bearer ${SERVICE_TOKEN}" \ - -H "X-Service-Token: ${SERVICE_TOKEN}" \ - "${full_url}" 2>&1) - - # Extract HTTP status - http_status=$(echo "$response" | grep "HTTP_STATUS" | cut -d':' -f2) - body=$(echo "$response" | sed '/HTTP_STATUS/d') - - if [ "$http_status" = "200" ]; then - # Parse total records if available - total_records=$(echo "$body" | grep -o '"total_records":[0-9]*' | cut -d':' -f2 || echo "N/A") - - print_success "$service_name: HTTP $http_status (Records: $total_records)" - PASSED_TESTS=$((PASSED_TESTS + 1)) - - # Show preview details if verbose - if [ "${VERBOSE:-0}" = "1" ]; then - echo "$body" | jq '.' 2>/dev/null || echo "$body" - fi - else - print_error "$service_name: HTTP $http_status" - FAILED_TESTS=$((FAILED_TESTS + 1)) - FAILED_SERVICES+=("$service_name") - - # Show error details - echo " URL: $full_url" - echo " Response: $body" | head -n 5 - fi -} - -# Main test execution -main() { - print_header "Tenant Deletion System - Integration Tests" - print_info "Testing tenant: $TENANT_ID" - print_info "Using service token: ${SERVICE_TOKEN:0:20}..." - echo "" - - # Test all services - print_header "Testing Individual Services (12 total)" - - test_service_preview "Orders" "$ORDERS_URL" "/orders" - test_service_preview "Inventory" "$INVENTORY_URL" "/inventory" - test_service_preview "Recipes" "$RECIPES_URL" "/recipes" - test_service_preview "Sales" "$SALES_URL" "/sales" - test_service_preview "Production" "$PRODUCTION_URL" "/production" - test_service_preview "Suppliers" "$SUPPLIERS_URL" "/suppliers" - test_service_preview "POS" "$POS_URL" "/pos" - test_service_preview "External" "$EXTERNAL_URL" "/external" - test_service_preview "Forecasting" "$FORECASTING_URL" "/forecasting" - test_service_preview "Training" "$TRAINING_URL" "/training" - test_service_preview "Alert Processor" "$ALERT_PROCESSOR_URL" "/alerts" - test_service_preview "Notification" "$NOTIFICATION_URL" "/notifications" - - # Print summary - echo "" - print_header "Test Summary" - echo -e "Total Tests: $TOTAL_TESTS" - echo -e "${GREEN}Passed: $PASSED_TESTS${NC}" - - if [ $FAILED_TESTS -gt 0 ]; then - echo -e "${RED}Failed: $FAILED_TESTS${NC}" - echo "" - print_error "Failed services:" - for service in "${FAILED_SERVICES[@]}"; do - echo " - $service" - done - echo "" - print_warning "Some services are not accessible or not implemented." - print_info "Make sure all services are running and URLs are correct." - exit 1 - else - echo -e "${GREEN}Failed: $FAILED_TESTS${NC}" - echo "" - print_success "All services passed! ✨" - exit 0 - fi -} - -# Check dependencies -check_dependencies() { - if ! command -v curl &> /dev/null; then - print_error "curl is required but not installed." - exit 1 - fi - - if ! command -v jq &> /dev/null; then - print_warning "jq not found. Install for better output formatting." - fi -} - -# Show usage -show_usage() { - cat << EOF -Usage: $0 [OPTIONS] [tenant_id] - -Test the tenant deletion system across all 12 microservices. - -Options: - -h, --help Show this help message - -v, --verbose Show detailed response bodies - -t, --tenant ID Specify tenant ID to test (default: demo tenant) - -Environment Variables: - SERVICE_TOKEN Service authentication token - *_URL Individual service URLs (e.g., ORDERS_URL) - -Examples: - # Test with default demo tenant - $0 - - # Test specific tenant - $0 abc-123-def-456 - - # Test with verbose output - VERBOSE=1 $0 - - # Test with custom service URLs - ORDERS_URL=http://orders:8000/api/v1/orders $0 - -EOF -} - -# Parse arguments -while [[ $# -gt 0 ]]; do - case $1 in - -h|--help) - show_usage - exit 0 - ;; - -v|--verbose) - VERBOSE=1 - shift - ;; - -t|--tenant) - TENANT_ID="$2" - shift 2 - ;; - *) - TENANT_ID="$1" - shift - ;; - esac -done - -# Run tests -check_dependencies -main diff --git a/scripts/validate_cross_refs.py b/scripts/validate_cross_refs.py deleted file mode 100644 index 4131b668..00000000 --- a/scripts/validate_cross_refs.py +++ /dev/null @@ -1,418 +0,0 @@ -#!/usr/bin/env python3 -""" -Cross-reference validation script for Bakery-IA demo data. -Validates UUID references across different services and fixtures. -""" - -import json -import os -import sys -from pathlib import Path -from typing import Dict, List, Any, Optional -from uuid import UUID - -# Configuration -BASE_DIR = Path(__file__).parent.parent / "shared" / "demo" -FIXTURES_DIR = BASE_DIR / "fixtures" / "professional" -METADATA_DIR = BASE_DIR / "metadata" - -class ValidationError(Exception): - """Custom exception for validation errors.""" - pass - -class CrossReferenceValidator: - def __init__(self): - self.fixtures = {} - self.cross_refs_map = {} - self.errors = [] - self.warnings = [] - - def load_fixtures(self): - """Load all fixture files.""" - fixture_files = [ - "01-tenant.json", "02-auth.json", "03-inventory.json", - "04-recipes.json", "05-suppliers.json", "06-production.json", - "07-procurement.json", "08-orders.json", "09-sales.json", - "10-forecasting.json" - ] - - for filename in fixture_files: - filepath = FIXTURES_DIR / filename - if filepath.exists(): - try: - with open(filepath, 'r', encoding='utf-8') as f: - self.fixtures[filename] = json.load(f) - except (json.JSONDecodeError, IOError) as e: - self.errors.append(f"Failed to load {filename}: {str(e)}") - else: - self.warnings.append(f"Fixture file {filename} not found") - - def load_cross_refs_map(self): - """Load cross-reference mapping from metadata.""" - map_file = METADATA_DIR / "cross_refs_map.json" - if map_file.exists(): - try: - with open(map_file, 'r', encoding='utf-8') as f: - data = json.load(f) - self.cross_refs_map = data.get("references", []) - except (json.JSONDecodeError, IOError) as e: - self.errors.append(f"Failed to load cross_refs_map.json: {str(e)}") - else: - self.errors.append("cross_refs_map.json not found") - - def is_valid_uuid(self, uuid_str: str) -> bool: - """Check if a string is a valid UUID.""" - try: - UUID(uuid_str) - return True - except ValueError: - return False - - def get_entity_by_id(self, service: str, entity_type: str, entity_id: str) -> Optional[Dict]: - """Find an entity by ID in the loaded fixtures.""" - # Map service names to fixture files - service_to_fixture = { - "inventory": "03-inventory.json", - "recipes": "04-recipes.json", - "suppliers": "05-suppliers.json", - "production": "06-production.json", - "procurement": "07-procurement.json", - "orders": "08-orders.json", - "sales": "09-sales.json", - "forecasting": "10-forecasting.json" - } - - if service not in service_to_fixture: - return None - - fixture_file = service_to_fixture[service] - if fixture_file not in self.fixtures: - return None - - fixture_data = self.fixtures[fixture_file] - - # Find the entity based on entity_type - if entity_type == "Ingredient": - return self._find_in_ingredients(fixture_data, entity_id) - elif entity_type == "Recipe": - return self._find_in_recipes(fixture_data, entity_id) - elif entity_type == "Supplier": - return self._find_in_suppliers(fixture_data, entity_id) - elif entity_type == "ProductionBatch": - return self._find_in_production_batches(fixture_data, entity_id) - elif entity_type == "PurchaseOrder": - return self._find_in_purchase_orders(fixture_data, entity_id) - elif entity_type == "Customer": - return self._find_in_customers(fixture_data, entity_id) - elif entity_type == "SalesData": - return self._find_in_sales_data(fixture_data, entity_id) - elif entity_type == "Forecast": - return self._find_in_forecasts(fixture_data, entity_id) - - return None - - def _find_in_ingredients(self, data: Dict, entity_id: str) -> Optional[Dict]: - """Find ingredient by ID.""" - if "ingredients" in data: - for ingredient in data["ingredients"]: - if ingredient.get("id") == entity_id: - return ingredient - return None - - def _find_in_recipes(self, data: Dict, entity_id: str) -> Optional[Dict]: - """Find recipe by ID.""" - if "recipes" in data: - for recipe in data["recipes"]: - if recipe.get("id") == entity_id: - return recipe - return None - - def _find_in_suppliers(self, data: Dict, entity_id: str) -> Optional[Dict]: - """Find supplier by ID.""" - if "suppliers" in data: - for supplier in data["suppliers"]: - if supplier.get("id") == entity_id: - return supplier - return None - - def _find_in_production_batches(self, data: Dict, entity_id: str) -> Optional[Dict]: - """Find production batch by ID.""" - if "production_batches" in data: - for batch in data["production_batches"]: - if batch.get("id") == entity_id: - return batch - return None - - def _find_in_purchase_orders(self, data: Dict, entity_id: str) -> Optional[Dict]: - """Find purchase order by ID.""" - if "purchase_orders" in data: - for po in data["purchase_orders"]: - if po.get("id") == entity_id: - return po - return None - - def _find_in_customers(self, data: Dict, entity_id: str) -> Optional[Dict]: - """Find customer by ID.""" - if "customers" in data: - for customer in data["customers"]: - if customer.get("id") == entity_id: - return customer - return None - - def _find_in_sales_data(self, data: Dict, entity_id: str) -> Optional[Dict]: - """Find sales data by ID.""" - if "sales_data" in data: - for sales in data["sales_data"]: - if sales.get("id") == entity_id: - return sales - return None - - def _find_in_forecasts(self, data: Dict, entity_id: str) -> Optional[Dict]: - """Find forecast by ID.""" - if "forecasts" in data: - for forecast in data["forecasts"]: - if forecast.get("id") == entity_id: - return forecast - return None - - def validate_cross_references(self): - """Validate all cross-references defined in the map.""" - for ref in self.cross_refs_map: - from_service = ref["from_service"] - from_entity = ref["from_entity"] - from_field = ref["from_field"] - to_service = ref["to_service"] - to_entity = ref["to_entity"] - required = ref.get("required", False) - - # Find all entities of the "from" type - entities = self._get_all_entities(from_service, from_entity) - - for entity in entities: - ref_id = entity.get(from_field) - if not ref_id: - if required: - self.errors.append( - f"{from_entity} {entity.get('id')} missing required field {from_field}" - ) - continue - - if not self.is_valid_uuid(ref_id): - self.errors.append( - f"{from_entity} {entity.get('id')} has invalid UUID in {from_field}: {ref_id}" - ) - continue - - # Check if the referenced entity exists - target_entity = self.get_entity_by_id(to_service, to_entity, ref_id) - if not target_entity: - if required: - self.errors.append( - f"{from_entity} {entity.get('id')} references non-existent {to_entity} {ref_id}" - ) - else: - self.warnings.append( - f"{from_entity} {entity.get('id')} references non-existent {to_entity} {ref_id}" - ) - continue - - # Check filters if specified - to_filter = ref.get("to_filter", {}) - if to_filter: - self._validate_filters_case_insensitive(target_entity, to_filter, entity, ref) - - def _get_all_entities(self, service: str, entity_type: str) -> List[Dict]: - """Get all entities of a specific type from a service.""" - entities = [] - - # Map entity types to fixture file and path - entity_mapping = { - "ProductionBatch": ("06-production.json", "production_batches"), - "RecipeIngredient": ("04-recipes.json", "recipe_ingredients"), - "Stock": ("03-inventory.json", "stock"), - "PurchaseOrder": ("07-procurement.json", "purchase_orders"), - "PurchaseOrderItem": ("07-procurement.json", "purchase_order_items"), - "OrderItem": ("08-orders.json", "order_items"), - "SalesData": ("09-sales.json", "sales_data"), - "Forecast": ("10-forecasting.json", "forecasts") - } - - if entity_type in entity_mapping: - fixture_file, path = entity_mapping[entity_type] - if fixture_file in self.fixtures: - data = self.fixtures[fixture_file] - if path in data: - return data[path] - - return entities - - def _validate_filters_case_insensitive(self, target_entity: Dict, filters: Dict, source_entity: Dict, ref: Dict): - """Validate that target entity matches specified filters (case-insensitive).""" - for filter_key, filter_value in filters.items(): - actual_value = target_entity.get(filter_key) - if actual_value is None: - self.errors.append( - f"{source_entity.get('id')} references {target_entity.get('id')} " - f"but {filter_key} is missing (expected {filter_value})" - ) - elif str(actual_value).lower() != str(filter_value).lower(): - self.errors.append( - f"{source_entity.get('id')} references {target_entity.get('id')} " - f"but {filter_key}={actual_value} != {filter_value}" - ) - - def validate_required_fields(self): - """Validate required fields in all fixtures.""" - required_fields_map = { - "01-tenant.json": { - "tenant": ["id", "name", "subscription_tier"] - }, - "02-auth.json": { - "users": ["id", "name", "email", "role"] - }, - "03-inventory.json": { - "ingredients": ["id", "name", "product_type", "ingredient_category"], - "stock": ["id", "ingredient_id", "quantity", "location"] - }, - "04-recipes.json": { - "recipes": ["id", "name", "status", "difficulty_level"], - "recipe_ingredients": ["id", "recipe_id", "ingredient_id", "quantity"] - }, - "05-suppliers.json": { - "suppliers": ["id", "name", "supplier_code", "status"] - }, - "06-production.json": { - "equipment": ["id", "name", "type", "status"], - "production_batches": ["id", "product_id", "status", "start_time"] - }, - "07-procurement.json": { - "purchase_orders": ["id", "po_number", "supplier_id", "status"], - "purchase_order_items": ["id", "purchase_order_id", "inventory_product_id", "ordered_quantity"] - }, - "08-orders.json": { - "customers": ["id", "customer_code", "name", "customer_type"], - "customer_orders": ["id", "customer_id", "order_number", "status"], - "order_items": ["id", "order_id", "product_id", "quantity"] - }, - "09-sales.json": { - "sales_data": ["id", "product_id", "quantity_sold", "unit_price"] - }, - "10-forecasting.json": { - "forecasts": ["id", "product_id", "forecast_date", "predicted_quantity"] - } - } - - for filename, required_structure in required_fields_map.items(): - if filename in self.fixtures: - data = self.fixtures[filename] - for entity_type, required_fields in required_structure.items(): - if entity_type in data: - entities = data[entity_type] - if isinstance(entities, list): - for entity in entities: - if isinstance(entity, dict): - for field in required_fields: - if field not in entity: - entity_id = entity.get('id', 'unknown') - self.errors.append( - f"{filename}: {entity_type} {entity_id} missing required field {field}" - ) - elif isinstance(entities, dict): - # Handle tenant which is a single dict - for field in required_fields: - if field not in entities: - entity_id = entities.get('id', 'unknown') - self.errors.append( - f"{filename}: {entity_type} {entity_id} missing required field {field}" - ) - - def validate_date_formats(self): - """Validate that all dates are in ISO format.""" - date_fields = [ - "created_at", "updated_at", "start_time", "end_time", - "order_date", "delivery_date", "expected_delivery_date", - "sale_date", "forecast_date", "contract_start_date", "contract_end_date" - ] - - for filename, data in self.fixtures.items(): - self._check_date_fields(data, date_fields, filename) - - def _check_date_fields(self, data: Any, date_fields: List[str], context: str): - """Recursively check for date fields.""" - if isinstance(data, dict): - for key, value in data.items(): - if key in date_fields and isinstance(value, str): - if not self._is_iso_format(value): - self.errors.append(f"{context}: Invalid date format in {key}: {value}") - elif isinstance(value, (dict, list)): - self._check_date_fields(value, date_fields, context) - elif isinstance(data, list): - for item in data: - self._check_date_fields(item, date_fields, context) - - def _is_iso_format(self, date_str: str) -> bool: - """Check if a string is in ISO format or BASE_TS marker.""" - try: - # Accept BASE_TS markers (e.g., "BASE_TS - 1h", "BASE_TS + 2d") - if date_str.startswith("BASE_TS"): - return True - - # Accept offset-based dates (used in some fixtures) - if "_offset_" in date_str: - return True - - # Simple check for ISO format (YYYY-MM-DDTHH:MM:SSZ or similar) - if len(date_str) < 19: - return False - return date_str.endswith('Z') and date_str[10] == 'T' - except: - return False - - def run_validation(self) -> bool: - """Run all validation checks.""" - print("🔍 Starting cross-reference validation...") - - # Load data - self.load_fixtures() - self.load_cross_refs_map() - - if self.errors: - print("❌ Errors during data loading:") - for error in self.errors: - print(f" - {error}") - return False - - # Run validation checks - print("📋 Validating cross-references...") - self.validate_cross_references() - - print("📝 Validating required fields...") - self.validate_required_fields() - - print("📅 Validating date formats...") - self.validate_date_formats() - - # Report results - if self.errors: - print(f"\n❌ Validation failed with {len(self.errors)} errors:") - for error in self.errors: - print(f" - {error}") - - if self.warnings: - print(f"\n⚠️ {len(self.warnings)} warnings:") - for warning in self.warnings: - print(f" - {warning}") - - return False - else: - print("\n✅ All validation checks passed!") - if self.warnings: - print(f"⚠️ {len(self.warnings)} warnings:") - for warning in self.warnings: - print(f" - {warning}") - return True - -if __name__ == "__main__": - validator = CrossReferenceValidator() - success = validator.run_validation() - sys.exit(0 if success else 1) \ No newline at end of file diff --git a/scripts/validate_demo_dates.py b/scripts/validate_demo_dates.py deleted file mode 100644 index c68a570a..00000000 --- a/scripts/validate_demo_dates.py +++ /dev/null @@ -1,242 +0,0 @@ -#!/usr/bin/env python3 -""" -Validate demo JSON files to ensure all dates use BASE_TS markers. -This script enforces the new architecture requirement that all temporal -data in demo fixtures must use BASE_TS markers for deterministic sessions. -""" - -import json -import sys -from pathlib import Path -from typing import Dict, List, Tuple, Set - -# Date/time fields that should use BASE_TS markers or be null -DATE_TIME_FIELDS = { - # Common fields - 'created_at', 'updated_at', - - # Procurement - 'order_date', 'required_delivery_date', 'estimated_delivery_date', - 'expected_delivery_date', 'sent_to_supplier_at', 'supplier_confirmation_date', - 'approval_deadline', - - # Production - 'planned_start_time', 'planned_end_time', 'actual_start_time', - 'actual_end_time', 'completed_at', 'install_date', 'last_maintenance_date', - 'next_maintenance_date', - - # Inventory - 'received_date', 'expiration_date', 'best_before_date', - 'original_expiration_date', 'transformation_date', 'final_expiration_date', - - # Orders - 'order_date', 'delivery_date', 'promised_date', 'last_order_date', - - # Forecasting - 'forecast_date', 'prediction_date', - - # Schedules - 'schedule_date', 'shift_start', 'shift_end', 'finalized_at', - - # Quality - 'check_time', - - # Generic - 'date', 'start_time', 'end_time' -} - - -class ValidationError: - """Represents a validation error""" - - def __init__(self, file_path: Path, entity_type: str, entity_index: int, - field_name: str, value: any, message: str): - self.file_path = file_path - self.entity_type = entity_type - self.entity_index = entity_index - self.field_name = field_name - self.value = value - self.message = message - - def __str__(self): - return ( - f"❌ {self.file_path.name} » {self.entity_type}[{self.entity_index}] » " - f"{self.field_name}: {self.message}\n" - f" Value: {self.value}" - ) - - -def validate_date_value(value: any, field_name: str) -> Tuple[bool, str]: - """ - Validate a single date field value. - - Returns: - (is_valid, error_message) - """ - # Null values are allowed - if value is None: - return True, "" - - # BASE_TS markers are the expected format - if isinstance(value, str) and value.startswith("BASE_TS"): - # Validate BASE_TS marker format - if value == "BASE_TS": - return True, "" - - # Should be "BASE_TS + ..." or "BASE_TS - ..." - parts = value.split() - if len(parts) < 3: - return False, f"Invalid BASE_TS marker format (expected 'BASE_TS +/- ')" - - if parts[1] not in ['+', '-']: - return False, f"Invalid BASE_TS operator (expected + or -)" - - # Extract offset parts (starting from index 2) - offset_parts = ' '.join(parts[2:]) - - # Validate offset components (must contain d, h, or m) - if not any(c in offset_parts for c in ['d', 'h', 'm']): - return False, f"BASE_TS offset must contain at least one of: d (days), h (hours), m (minutes)" - - return True, "" - - # ISO 8601 timestamps are NOT allowed (should use BASE_TS) - if isinstance(value, str) and ('T' in value or 'Z' in value): - return False, "Found ISO 8601 timestamp - should use BASE_TS marker instead" - - # offset_days dictionaries are NOT allowed (legacy format) - if isinstance(value, dict) and 'offset_days' in value: - return False, "Found offset_days dictionary - should use BASE_TS marker instead" - - # Unknown format - return False, f"Unknown date format (type: {type(value).__name__})" - - -def validate_entity(entity: Dict, entity_type: str, entity_index: int, - file_path: Path) -> List[ValidationError]: - """ - Validate all date fields in a single entity. - - Returns: - List of validation errors - """ - errors = [] - - # Check for legacy offset_days fields - for key in entity.keys(): - if key.endswith('_offset_days'): - base_field = key.replace('_offset_days', '') - errors.append(ValidationError( - file_path, entity_type, entity_index, key, - entity[key], - f"Legacy offset_days field found - migrate to BASE_TS marker in '{base_field}' field" - )) - - # Validate date/time fields - for field_name, value in entity.items(): - if field_name in DATE_TIME_FIELDS: - is_valid, error_msg = validate_date_value(value, field_name) - if not is_valid: - errors.append(ValidationError( - file_path, entity_type, entity_index, field_name, - value, error_msg - )) - - return errors - - -def validate_json_file(file_path: Path) -> List[ValidationError]: - """ - Validate all entities in a JSON file. - - Returns: - List of validation errors - """ - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - except json.JSONDecodeError as e: - return [ValidationError( - file_path, "FILE", 0, "JSON", - None, f"Invalid JSON: {e}" - )] - except Exception as e: - return [ValidationError( - file_path, "FILE", 0, "READ", - None, f"Failed to read file: {e}" - )] - - errors = [] - - # Validate each entity type - for entity_type, entities in data.items(): - if isinstance(entities, list): - for i, entity in enumerate(entities): - if isinstance(entity, dict): - errors.extend( - validate_entity(entity, entity_type, i, file_path) - ) - - return errors - - -def main(): - """Main validation function""" - # Find all JSON files in demo fixtures - root_dir = Path(__file__).parent.parent - fixtures_dir = root_dir / "shared" / "demo" / "fixtures" - - if not fixtures_dir.exists(): - print(f"❌ Fixtures directory not found: {fixtures_dir}") - return 1 - - # Find all JSON files - json_files = sorted(fixtures_dir.rglob("*.json")) - - if not json_files: - print(f"❌ No JSON files found in {fixtures_dir}") - return 1 - - print(f"🔍 Validating {len(json_files)} JSON files...\n") - - # Validate each file - all_errors = [] - files_with_errors = 0 - - for json_file in json_files: - errors = validate_json_file(json_file) - - if errors: - files_with_errors += 1 - all_errors.extend(errors) - - # Print file header - relative_path = json_file.relative_to(fixtures_dir) - print(f"\n📄 {relative_path}") - print(f" Found {len(errors)} error(s):") - - # Print each error - for error in errors: - print(f" {error}") - - # Print summary - print("\n" + "=" * 80) - if all_errors: - print(f"\n❌ VALIDATION FAILED") - print(f" Total errors: {len(all_errors)}") - print(f" Files with errors: {files_with_errors}/{len(json_files)}") - print(f"\n💡 Fix these errors by:") - print(f" 1. Replacing ISO timestamps with BASE_TS markers") - print(f" 2. Removing *_offset_days fields") - print(f" 3. Using format: 'BASE_TS +/- ' where offset uses d/h/m") - print(f" Examples: 'BASE_TS', 'BASE_TS + 2d', 'BASE_TS - 4h', 'BASE_TS + 1h30m'") - return 1 - else: - print(f"\n✅ ALL VALIDATIONS PASSED") - print(f" Files validated: {len(json_files)}") - print(f" All date fields use BASE_TS markers correctly") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/validate_demo_seeding.sh b/scripts/validate_demo_seeding.sh deleted file mode 100755 index b1ed24ca..00000000 --- a/scripts/validate_demo_seeding.sh +++ /dev/null @@ -1,297 +0,0 @@ -#!/bin/bash -# validate_demo_seeding.sh -# Comprehensive smoke test for demo seeding validation -# Tests both Professional and Enterprise demo templates - -set -e # Exit on error - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Counters -TESTS_PASSED=0 -TESTS_FAILED=0 -TESTS_TOTAL=0 - -# Fixed Demo Tenant IDs -DEMO_TENANT_PROFESSIONAL="a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6" -DEMO_TENANT_ENTERPRISE_PARENT="c3d4e5f6-a7b8-49c0-d1e2-f3a4b5c6d7e8" -DEMO_TENANT_CHILD_1="d4e5f6a7-b8c9-40d1-e2f3-a4b5c6d7e8f9" -DEMO_TENANT_CHILD_2="e5f6a7b8-c9d0-41e2-f3a4-b5c6d7e8f9a0" -DEMO_TENANT_CHILD_3="f6a7b8c9-d0e1-42f3-a4b5-c6d7e8f9a0b1" - -# Database connection strings (from Kubernetes secrets) -get_db_url() { - local service=$1 - kubectl get secret database-secrets -n bakery-ia -o jsonpath="{.data.${service}_DATABASE_URL}" | base64 -d -} - -# Test helper functions -test_start() { - TESTS_TOTAL=$((TESTS_TOTAL + 1)) - echo -e "${BLUE}[TEST $TESTS_TOTAL]${NC} $1" -} - -test_pass() { - TESTS_PASSED=$((TESTS_PASSED + 1)) - echo -e " ${GREEN}✓ PASS${NC}: $1" -} - -test_fail() { - TESTS_FAILED=$((TESTS_FAILED + 1)) - echo -e " ${RED}✗ FAIL${NC}: $1" -} - -test_warn() { - echo -e " ${YELLOW}⚠ WARN${NC}: $1" -} - -# SQL query helper -query_db() { - local db_url=$1 - local query=$2 - kubectl run psql-temp-$RANDOM --rm -i --restart=Never --image=postgres:17-alpine -- \ - psql "$db_url" -t -c "$query" 2>/dev/null | xargs -} - -echo "========================================" -echo "🧪 Demo Seeding Validation Test Suite" -echo "========================================" -echo "" -echo "Testing Professional and Enterprise demo templates..." -echo "" - -# ============================================================================= -# PHASE 1: PROFESSIONAL TIER VALIDATION -# ============================================================================= -echo "========================================" -echo "📦 Phase 1: Professional Tier (Single Bakery)" -echo "========================================" -echo "" - -# Test 1: Tenant Service - Professional tenant exists -test_start "Professional tenant exists in tenant service" -TENANT_DB=$(get_db_url "TENANT") -TENANT_COUNT=$(query_db "$TENANT_DB" "SELECT COUNT(*) FROM tenants WHERE id='$DEMO_TENANT_PROFESSIONAL' AND business_model='individual_bakery'") -if [ "$TENANT_COUNT" -eq 1 ]; then - test_pass "Professional tenant found (Panadería Artesana Madrid)" -else - test_fail "Professional tenant not found or incorrect count: $TENANT_COUNT" -fi - -# Test 2: Inventory - Professional has raw ingredients -test_start "Professional tenant has raw ingredients" -INVENTORY_DB=$(get_db_url "INVENTORY") -INGREDIENT_COUNT=$(query_db "$INVENTORY_DB" "SELECT COUNT(*) FROM ingredients WHERE tenant_id='$DEMO_TENANT_PROFESSIONAL' AND product_type='INGREDIENT'") -if [ "$INGREDIENT_COUNT" -ge 20 ]; then - test_pass "Found $INGREDIENT_COUNT raw ingredients (expected ~24)" -else - test_fail "Insufficient raw ingredients: $INGREDIENT_COUNT (expected >=20)" -fi - -# Test 3: Inventory - Professional has finished products -test_start "Professional tenant has finished products" -PRODUCT_COUNT=$(query_db "$INVENTORY_DB" "SELECT COUNT(*) FROM ingredients WHERE tenant_id='$DEMO_TENANT_PROFESSIONAL' AND product_type='FINISHED_PRODUCT'") -if [ "$PRODUCT_COUNT" -ge 4 ]; then - test_pass "Found $PRODUCT_COUNT finished products (expected ~4)" -else - test_fail "Insufficient finished products: $PRODUCT_COUNT (expected >=4)" -fi - -# Test 4: Recipes - Professional has recipes -test_start "Professional tenant has recipes" -RECIPES_DB=$(get_db_url "RECIPES") -RECIPE_COUNT=$(query_db "$RECIPES_DB" "SELECT COUNT(*) FROM recipes WHERE tenant_id='$DEMO_TENANT_PROFESSIONAL'") -if [ "$RECIPE_COUNT" -ge 4 ]; then - test_pass "Found $RECIPE_COUNT recipes (expected ~4-20)" -else - test_fail "Insufficient recipes: $RECIPE_COUNT (expected >=4)" -fi - -# Test 5: Sales - Professional has sales history -test_start "Professional tenant has sales history" -SALES_DB=$(get_db_url "SALES") -SALES_COUNT=$(query_db "$SALES_DB" "SELECT COUNT(*) FROM sales_data WHERE tenant_id='$DEMO_TENANT_PROFESSIONAL'") -if [ "$SALES_COUNT" -ge 100 ]; then - test_pass "Found $SALES_COUNT sales records (expected ~360 for 90 days)" -else - test_warn "Lower than expected sales records: $SALES_COUNT (expected >=100)" -fi - -# ============================================================================= -# PHASE 2: ENTERPRISE PARENT VALIDATION -# ============================================================================= -echo "" -echo "========================================" -echo "🏭 Phase 2: Enterprise Parent (Obrador)" -echo "========================================" -echo "" - -# Test 6: Tenant Service - Enterprise parent exists -test_start "Enterprise parent tenant exists" -PARENT_COUNT=$(query_db "$TENANT_DB" "SELECT COUNT(*) FROM tenants WHERE id='$DEMO_TENANT_ENTERPRISE_PARENT' AND business_model='enterprise_chain'") -if [ "$PARENT_COUNT" -eq 1 ]; then - test_pass "Enterprise parent found (Obrador Madrid)" -else - test_fail "Enterprise parent not found or incorrect count: $PARENT_COUNT" -fi - -# Test 7: Inventory - Parent has raw ingredients (scaled 10x) -test_start "Enterprise parent has raw ingredients" -PARENT_INGREDIENT_COUNT=$(query_db "$INVENTORY_DB" "SELECT COUNT(*) FROM ingredients WHERE tenant_id='$DEMO_TENANT_ENTERPRISE_PARENT' AND product_type='INGREDIENT'") -if [ "$PARENT_INGREDIENT_COUNT" -ge 20 ]; then - test_pass "Found $PARENT_INGREDIENT_COUNT raw ingredients (expected ~24)" -else - test_fail "Insufficient parent raw ingredients: $PARENT_INGREDIENT_COUNT (expected >=20)" -fi - -# Test 8: Recipes - Parent has recipes -test_start "Enterprise parent has recipes" -PARENT_RECIPE_COUNT=$(query_db "$RECIPES_DB" "SELECT COUNT(*) FROM recipes WHERE tenant_id='$DEMO_TENANT_ENTERPRISE_PARENT'") -if [ "$PARENT_RECIPE_COUNT" -ge 4 ]; then - test_pass "Found $PARENT_RECIPE_COUNT recipes (expected ~4-20)" -else - test_fail "Insufficient parent recipes: $PARENT_RECIPE_COUNT (expected >=4)" -fi - -# Test 9: Production - Parent has production batches -test_start "Enterprise parent has production batches" -PRODUCTION_DB=$(get_db_url "PRODUCTION") -BATCH_COUNT=$(query_db "$PRODUCTION_DB" "SELECT COUNT(*) FROM production_batches WHERE tenant_id='$DEMO_TENANT_ENTERPRISE_PARENT'") -if [ "$BATCH_COUNT" -ge 50 ]; then - test_pass "Found $BATCH_COUNT production batches (expected ~120)" -elif [ "$BATCH_COUNT" -ge 20 ]; then - test_warn "Lower production batches: $BATCH_COUNT (expected ~120)" -else - test_fail "Insufficient production batches: $BATCH_COUNT (expected >=50)" -fi - -# ============================================================================= -# PHASE 3: CHILD RETAIL OUTLETS VALIDATION -# ============================================================================= -echo "" -echo "========================================" -echo "🏪 Phase 3: Child Retail Outlets" -echo "========================================" -echo "" - -# Test each child tenant -for CHILD_ID in "$DEMO_TENANT_CHILD_1" "$DEMO_TENANT_CHILD_2" "$DEMO_TENANT_CHILD_3"; do - case "$CHILD_ID" in - "$DEMO_TENANT_CHILD_1") CHILD_NAME="Madrid Centro" ;; - "$DEMO_TENANT_CHILD_2") CHILD_NAME="Barcelona Gràcia" ;; - "$DEMO_TENANT_CHILD_3") CHILD_NAME="Valencia Ruzafa" ;; - esac - - echo "" - echo "Testing: $CHILD_NAME" - echo "----------------------------------------" - - # Test 10a: Child has finished products ONLY (no raw ingredients) - test_start "[$CHILD_NAME] Has finished products ONLY" - CHILD_PRODUCTS=$(query_db "$INVENTORY_DB" "SELECT COUNT(*) FROM ingredients WHERE tenant_id='$CHILD_ID' AND product_type='FINISHED_PRODUCT'") - CHILD_RAW=$(query_db "$INVENTORY_DB" "SELECT COUNT(*) FROM ingredients WHERE tenant_id='$CHILD_ID' AND product_type='INGREDIENT'") - - if [ "$CHILD_PRODUCTS" -eq 4 ] && [ "$CHILD_RAW" -eq 0 ]; then - test_pass "Found $CHILD_PRODUCTS finished products, 0 raw ingredients (correct retail model)" - elif [ "$CHILD_RAW" -gt 0 ]; then - test_fail "Child has raw ingredients ($CHILD_RAW) - should only have finished products" - else - test_warn "Product count mismatch: $CHILD_PRODUCTS (expected 4)" - fi - - # Test 10b: Child has stock batches - test_start "[$CHILD_NAME] Has stock batches" - CHILD_STOCK=$(query_db "$INVENTORY_DB" "SELECT COUNT(*) FROM stock WHERE tenant_id='$CHILD_ID'") - if [ "$CHILD_STOCK" -ge 10 ]; then - test_pass "Found $CHILD_STOCK stock batches (expected ~16)" - else - test_warn "Lower stock batches: $CHILD_STOCK (expected ~16)" - fi - - # Test 10c: Child has sales history - test_start "[$CHILD_NAME] Has sales history" - CHILD_SALES=$(query_db "$SALES_DB" "SELECT COUNT(*) FROM sales_data WHERE tenant_id='$CHILD_ID'") - if [ "$CHILD_SALES" -ge 80 ]; then - test_pass "Found $CHILD_SALES sales records (expected ~120 for 30 days)" - else - test_warn "Lower sales records: $CHILD_SALES (expected ~120)" - fi - - # Test 10d: Child has customers - test_start "[$CHILD_NAME] Has walk-in customers" - ORDERS_DB=$(get_db_url "ORDERS") - CHILD_CUSTOMERS=$(query_db "$ORDERS_DB" "SELECT COUNT(*) FROM customers WHERE tenant_id='$CHILD_ID'") - if [ "$CHILD_CUSTOMERS" -ge 40 ]; then - test_pass "Found $CHILD_CUSTOMERS customers (expected 60-100)" - else - test_warn "Lower customer count: $CHILD_CUSTOMERS (expected 60-100)" - fi -done - -# ============================================================================= -# PHASE 4: DISTRIBUTION VALIDATION -# ============================================================================= -echo "" -echo "========================================" -echo "🚚 Phase 4: Distribution & Logistics" -echo "========================================" -echo "" - -# Test 11: Distribution routes exist -test_start "Distribution routes created (Mon/Wed/Fri pattern)" -DISTRIBUTION_DB=$(get_db_url "DISTRIBUTION") -ROUTE_COUNT=$(query_db "$DISTRIBUTION_DB" "SELECT COUNT(*) FROM delivery_routes WHERE tenant_id='$DEMO_TENANT_ENTERPRISE_PARENT'") -if [ "$ROUTE_COUNT" -ge 10 ]; then - test_pass "Found $ROUTE_COUNT delivery routes (expected ~13 for 30 days, Mon/Wed/Fri)" -else - test_warn "Lower route count: $ROUTE_COUNT (expected ~13)" -fi - -# Test 12: Shipments exist for all children -test_start "Shipments created for all retail outlets" -SHIPMENT_COUNT=$(query_db "$DISTRIBUTION_DB" "SELECT COUNT(*) FROM shipments WHERE parent_tenant_id='$DEMO_TENANT_ENTERPRISE_PARENT'") -if [ "$SHIPMENT_COUNT" -ge 30 ]; then - test_pass "Found $SHIPMENT_COUNT shipments (expected ~39: 13 routes × 3 children)" -else - test_warn "Lower shipment count: $SHIPMENT_COUNT (expected ~39)" -fi - -# ============================================================================= -# SUMMARY -# ============================================================================= -echo "" -echo "========================================" -echo "📊 Test Summary" -echo "========================================" -echo "" -echo "Total Tests: $TESTS_TOTAL" -echo -e "${GREEN}Passed: $TESTS_PASSED${NC}" -echo -e "${RED}Failed: $TESTS_FAILED${NC}" -echo "" - -if [ $TESTS_FAILED -eq 0 ]; then - echo -e "${GREEN}✅ ALL TESTS PASSED!${NC}" - echo "" - echo "Demo templates are ready for cloning:" - echo " ✓ Professional tier (single bakery): ~3,500 records" - echo " ✓ Enterprise parent (Obrador): ~3,000 records" - echo " ✓ 3 Child retail outlets: ~700 records" - echo " ✓ Distribution history: ~52 records" - echo " ✓ Total template data: ~4,200-4,800 records" - echo "" - exit 0 -else - echo -e "${RED}❌ SOME TESTS FAILED${NC}" - echo "" - echo "Please review the failed tests above and:" - echo " 1. Check that all seed jobs completed successfully" - echo " 2. Verify database connections" - echo " 3. Check seed script logs for errors" - echo "" - exit 1 -fi diff --git a/scripts/validate_enterprise_demo_fixtures.py b/scripts/validate_enterprise_demo_fixtures.py deleted file mode 100755 index 5d8aae4d..00000000 --- a/scripts/validate_enterprise_demo_fixtures.py +++ /dev/null @@ -1,584 +0,0 @@ -#!/usr/bin/env python3 -""" -Enterprise Demo Fixtures Validation Script - -Validates cross-references between JSON fixtures for enterprise demo sessions. -Checks that all referenced IDs exist and are consistent across files. -""" - -import json -import os -import sys -from pathlib import Path -from typing import Dict, List, Set, Any, Optional -from collections import defaultdict -import uuid - -# Color codes for output -RED = '\033[91m' -GREEN = '\033[92m' -YELLOW = '\033[93m' -BLUE = '\033[94m' -RESET = '\033[0m' - -class FixtureValidator: - def __init__(self, base_path: str = "shared/demo/fixtures/enterprise"): - self.base_path = Path(base_path) - self.parent_path = self.base_path / "parent" - self.children_paths = {} - - # Load all fixture data - self.tenant_data = {} - self.user_data = {} - self.location_data = {} - self.product_data = {} - self.supplier_data = {} - self.recipe_data = {} - self.procurement_data = {} - self.order_data = {} - self.production_data = {} - - # Track all IDs for validation - self.all_ids = defaultdict(set) - self.references = defaultdict(list) - - # Expected IDs from tenant.json - self.expected_tenant_ids = set() - self.expected_user_ids = set() - self.expected_location_ids = set() - - def load_all_fixtures(self) -> None: - """Load all JSON fixtures from parent and children directories""" - print(f"{BLUE}Loading fixtures from {self.base_path}{RESET}") - - # Load parent fixtures - self._load_parent_fixtures() - - # Load children fixtures - self._load_children_fixtures() - - print(f"{GREEN}✓ Loaded fixtures successfully{RESET}\n") - - def _load_parent_fixtures(self) -> None: - """Load parent enterprise fixtures""" - if not self.parent_path.exists(): - print(f"{RED}✗ Parent fixtures directory not found: {self.parent_path}{RESET}") - sys.exit(1) - - # Load in order to establish dependencies - files_to_load = [ - "01-tenant.json", - "02-auth.json", - "03-inventory.json", - "04-recipes.json", - "05-suppliers.json", - "06-production.json", - "07-procurement.json", - "08-orders.json", - "09-sales.json", - "10-forecasting.json", - "11-orchestrator.json" - ] - - for filename in files_to_load: - filepath = self.parent_path / filename - if filepath.exists(): - with open(filepath, 'r', encoding='utf-8') as f: - data = json.load(f) - self._process_fixture_file(filename, data, "parent") - - def _load_children_fixtures(self) -> None: - """Load children enterprise fixtures""" - children_dir = self.base_path / "children" - if not children_dir.exists(): - print(f"{YELLOW}⚠ Children fixtures directory not found: {children_dir}{RESET}") - return - - # Find all child tenant directories - child_dirs = [d for d in children_dir.iterdir() if d.is_dir()] - - for child_dir in child_dirs: - tenant_id = child_dir.name - self.children_paths[tenant_id] = child_dir - - # Load child fixtures - files_to_load = [ - "01-tenant.json", - "02-auth.json", - "03-inventory.json", - "04-recipes.json", - "05-suppliers.json", - "06-production.json", - "07-procurement.json", - "08-orders.json", - "09-sales.json", - "10-forecasting.json", - "11-orchestrator.json" - ] - - for filename in files_to_load: - filepath = child_dir / filename - if filepath.exists(): - with open(filepath, 'r', encoding='utf-8') as f: - data = json.load(f) - self._process_fixture_file(filename, data, tenant_id) - - def _process_fixture_file(self, filename: str, data: Any, context: str) -> None: - """Process a fixture file and extract IDs and references""" - print(f" Processing {filename} ({context})...") - - if filename == "01-tenant.json": - self._process_tenant_data(data, context) - elif filename == "02-auth.json": - self._process_auth_data(data, context) - elif filename == "03-inventory.json": - self._process_inventory_data(data, context) - elif filename == "04-recipes.json": - self._process_recipe_data(data, context) - elif filename == "05-suppliers.json": - self._process_supplier_data(data, context) - elif filename == "06-production.json": - self._process_production_data(data, context) - elif filename == "07-procurement.json": - self._process_procurement_data(data, context) - elif filename == "08-orders.json": - self._process_order_data(data, context) - elif filename == "09-sales.json": - self._process_sales_data(data, context) - elif filename == "10-forecasting.json": - self._process_forecasting_data(data, context) - elif filename == "11-orchestrator.json": - self._process_orchestrator_data(data, context) - - def _process_tenant_data(self, data: Any, context: str) -> None: - """Process tenant.json data""" - tenant = data.get("tenant", {}) - owner = data.get("owner", {}) - subscription = data.get("subscription", {}) - children = data.get("children", []) - - # Store tenant data - tenant_id = tenant.get("id") - if tenant_id: - self.tenant_data[tenant_id] = tenant - self.all_ids["tenant"].add(tenant_id) - - if context == "parent": - self.expected_tenant_ids.add(tenant_id) - - # Store owner user - owner_id = owner.get("id") - if owner_id: - self.user_data[owner_id] = owner - self.all_ids["user"].add(owner_id) - self.expected_user_ids.add(owner_id) - - # Store subscription - subscription_id = subscription.get("id") - if subscription_id: - self.all_ids["subscription"].add(subscription_id) - - # Store child tenants - for child in children: - child_id = child.get("id") - if child_id: - self.tenant_data[child_id] = child - self.all_ids["tenant"].add(child_id) - self.expected_tenant_ids.add(child_id) - - # Track parent-child relationship - self.references["parent_child"].append({ - "parent": tenant_id, - "child": child_id, - "context": context - }) - - def _process_auth_data(self, data: Any, context: str) -> None: - """Process auth.json data""" - users = data.get("users", []) - - for user in users: - user_id = user.get("id") - tenant_id = user.get("tenant_id") - - if user_id: - self.user_data[user_id] = user - self.all_ids["user"].add(user_id) - self.expected_user_ids.add(user_id) - - # Track user-tenant relationship - if tenant_id: - self.references["user_tenant"].append({ - "user_id": user_id, - "tenant_id": tenant_id, - "context": context - }) - - def _process_inventory_data(self, data: Any, context: str) -> None: - """Process inventory.json data""" - products = data.get("products", []) - ingredients = data.get("ingredients", []) - locations = data.get("locations", []) - - # Store products - for product in products: - product_id = product.get("id") - tenant_id = product.get("tenant_id") - created_by = product.get("created_by") - - if product_id: - self.product_data[product_id] = product - self.all_ids["product"].add(product_id) - - # Track product-tenant relationship - if tenant_id: - self.references["product_tenant"].append({ - "product_id": product_id, - "tenant_id": tenant_id, - "context": context - }) - - # Track product-user relationship - if created_by: - self.references["product_user"].append({ - "product_id": product_id, - "user_id": created_by, - "context": context - }) - - # Store ingredients - for ingredient in ingredients: - ingredient_id = ingredient.get("id") - tenant_id = ingredient.get("tenant_id") - created_by = ingredient.get("created_by") - - if ingredient_id: - self.product_data[ingredient_id] = ingredient - self.all_ids["ingredient"].add(ingredient_id) - - # Track ingredient-tenant relationship - if tenant_id: - self.references["ingredient_tenant"].append({ - "ingredient_id": ingredient_id, - "tenant_id": tenant_id, - "context": context - }) - - # Track ingredient-user relationship - if created_by: - self.references["ingredient_user"].append({ - "ingredient_id": ingredient_id, - "user_id": created_by, - "context": context - }) - - # Store locations - for location in locations: - location_id = location.get("id") - if location_id: - self.location_data[location_id] = location - self.all_ids["location"].add(location_id) - self.expected_location_ids.add(location_id) - - def _process_recipe_data(self, data: Any, context: str) -> None: - """Process recipes.json data""" - recipes = data.get("recipes", []) - - for recipe in recipes: - recipe_id = recipe.get("id") - tenant_id = recipe.get("tenant_id") - finished_product_id = recipe.get("finished_product_id") - - if recipe_id: - self.recipe_data[recipe_id] = recipe - self.all_ids["recipe"].add(recipe_id) - - # Track recipe-tenant relationship - if tenant_id: - self.references["recipe_tenant"].append({ - "recipe_id": recipe_id, - "tenant_id": tenant_id, - "context": context - }) - - # Track recipe-product relationship - if finished_product_id: - self.references["recipe_product"].append({ - "recipe_id": recipe_id, - "product_id": finished_product_id, - "context": context - }) - - def _process_supplier_data(self, data: Any, context: str) -> None: - """Process suppliers.json data""" - suppliers = data.get("suppliers", []) - - for supplier in suppliers: - supplier_id = supplier.get("id") - tenant_id = supplier.get("tenant_id") - - if supplier_id: - self.supplier_data[supplier_id] = supplier - self.all_ids["supplier"].add(supplier_id) - - # Track supplier-tenant relationship - if tenant_id: - self.references["supplier_tenant"].append({ - "supplier_id": supplier_id, - "tenant_id": tenant_id, - "context": context - }) - - def _process_production_data(self, data: Any, context: str) -> None: - """Process production.json data""" - # Extract production-related IDs - pass - - def _process_procurement_data(self, data: Any, context: str) -> None: - """Process procurement.json data""" - # Extract procurement-related IDs - pass - - def _process_order_data(self, data: Any, context: str) -> None: - """Process orders.json data""" - # Extract order-related IDs - pass - - def _process_sales_data(self, data: Any, context: str) -> None: - """Process sales.json data""" - # Extract sales-related IDs - pass - - def _process_forecasting_data(self, data: Any, context: str) -> None: - """Process forecasting.json data""" - # Extract forecasting-related IDs - pass - - def _process_orchestrator_data(self, data: Any, context: str) -> None: - """Process orchestrator.json data""" - # Extract orchestrator-related IDs - pass - - def validate_all_references(self) -> bool: - """Validate all cross-references in the fixtures""" - print(f"{BLUE}Validating cross-references...{RESET}") - - all_valid = True - - # Validate user-tenant relationships - if "user_tenant" in self.references: - print(f"\n{YELLOW}Validating User-Tenant relationships...{RESET}") - for ref in self.references["user_tenant"]: - user_id = ref["user_id"] - tenant_id = ref["tenant_id"] - context = ref["context"] - - if user_id not in self.user_data: - print(f"{RED}✗ User {user_id} referenced but not found in user data (context: {context}){RESET}") - all_valid = False - - if tenant_id not in self.tenant_data: - print(f"{RED}✗ Tenant {tenant_id} referenced by user {user_id} but not found (context: {context}){RESET}") - all_valid = False - - # Validate parent-child relationships - if "parent_child" in self.references: - print(f"\n{YELLOW}Validating Parent-Child relationships...{RESET}") - for ref in self.references["parent_child"]: - parent_id = ref["parent"] - child_id = ref["child"] - context = ref["context"] - - if parent_id not in self.tenant_data: - print(f"{RED}✗ Parent tenant {parent_id} not found (context: {context}){RESET}") - all_valid = False - - if child_id not in self.tenant_data: - print(f"{RED}✗ Child tenant {child_id} not found (context: {context}){RESET}") - all_valid = False - - # Validate product-tenant relationships - if "product_tenant" in self.references: - print(f"\n{YELLOW}Validating Product-Tenant relationships...{RESET}") - for ref in self.references["product_tenant"]: - product_id = ref["product_id"] - tenant_id = ref["tenant_id"] - context = ref["context"] - - if product_id not in self.product_data: - print(f"{RED}✗ Product {product_id} referenced but not found (context: {context}){RESET}") - all_valid = False - - if tenant_id not in self.tenant_data: - print(f"{RED}✗ Tenant {tenant_id} referenced by product {product_id} but not found (context: {context}){RESET}") - all_valid = False - - # Validate product-user relationships - if "product_user" in self.references: - print(f"\n{YELLOW}Validating Product-User relationships...{RESET}") - for ref in self.references["product_user"]: - product_id = ref["product_id"] - user_id = ref["user_id"] - context = ref["context"] - - if product_id not in self.product_data: - print(f"{RED}✗ Product {product_id} referenced but not found (context: {context}){RESET}") - all_valid = False - - if user_id not in self.user_data: - print(f"{RED}✗ User {user_id} referenced by product {product_id} but not found (context: {context}){RESET}") - all_valid = False - - # Validate ingredient-tenant relationships - if "ingredient_tenant" in self.references: - print(f"\n{YELLOW}Validating Ingredient-Tenant relationships...{RESET}") - for ref in self.references["ingredient_tenant"]: - ingredient_id = ref["ingredient_id"] - tenant_id = ref["tenant_id"] - context = ref["context"] - - if ingredient_id not in self.product_data: - print(f"{RED}✗ Ingredient {ingredient_id} referenced but not found (context: {context}){RESET}") - all_valid = False - - if tenant_id not in self.tenant_data: - print(f"{RED}✗ Tenant {tenant_id} referenced by ingredient {ingredient_id} but not found (context: {context}){RESET}") - all_valid = False - - # Validate ingredient-user relationships - if "ingredient_user" in self.references: - print(f"\n{YELLOW}Validating Ingredient-User relationships...{RESET}") - for ref in self.references["ingredient_user"]: - ingredient_id = ref["ingredient_id"] - user_id = ref["user_id"] - context = ref["context"] - - if ingredient_id not in self.product_data: - print(f"{RED}✗ Ingredient {ingredient_id} referenced but not found (context: {context}){RESET}") - all_valid = False - - if user_id not in self.user_data: - print(f"{RED}✗ User {user_id} referenced by ingredient {ingredient_id} but not found (context: {context}){RESET}") - all_valid = False - - # Validate recipe-tenant relationships - if "recipe_tenant" in self.references: - print(f"\n{YELLOW}Validating Recipe-Tenant relationships...{RESET}") - for ref in self.references["recipe_tenant"]: - recipe_id = ref["recipe_id"] - tenant_id = ref["tenant_id"] - context = ref["context"] - - if recipe_id not in self.recipe_data: - print(f"{RED}✗ Recipe {recipe_id} referenced but not found (context: {context}){RESET}") - all_valid = False - - if tenant_id not in self.tenant_data: - print(f"{RED}✗ Tenant {tenant_id} referenced by recipe {recipe_id} but not found (context: {context}){RESET}") - all_valid = False - - # Validate recipe-product relationships - if "recipe_product" in self.references: - print(f"\n{YELLOW}Validating Recipe-Product relationships...{RESET}") - for ref in self.references["recipe_product"]: - recipe_id = ref["recipe_id"] - product_id = ref["product_id"] - context = ref["context"] - - if recipe_id not in self.recipe_data: - print(f"{RED}✗ Recipe {recipe_id} referenced but not found (context: {context}){RESET}") - all_valid = False - - if product_id not in self.product_data: - print(f"{RED}✗ Product {product_id} referenced by recipe {recipe_id} but not found (context: {context}){RESET}") - all_valid = False - - # Validate supplier-tenant relationships - if "supplier_tenant" in self.references: - print(f"\n{YELLOW}Validating Supplier-Tenant relationships...{RESET}") - for ref in self.references["supplier_tenant"]: - supplier_id = ref["supplier_id"] - tenant_id = ref["tenant_id"] - context = ref["context"] - - if supplier_id not in self.supplier_data: - print(f"{RED}✗ Supplier {supplier_id} referenced but not found (context: {context}){RESET}") - all_valid = False - - if tenant_id not in self.tenant_data: - print(f"{RED}✗ Tenant {tenant_id} referenced by supplier {supplier_id} but not found (context: {context}){RESET}") - all_valid = False - - # Validate UUID format for all IDs - print(f"\n{YELLOW}Validating UUID formats...{RESET}") - for entity_type, ids in self.all_ids.items(): - for entity_id in ids: - try: - uuid.UUID(entity_id) - except ValueError: - print(f"{RED}✗ Invalid UUID format for {entity_type} ID: {entity_id}{RESET}") - all_valid = False - - # Check for duplicate IDs - print(f"\n{YELLOW}Checking for duplicate IDs...{RESET}") - all_entities = [] - for ids in self.all_ids.values(): - all_entities.extend(ids) - - duplicates = [id for id in all_entities if all_entities.count(id) > 1] - if duplicates: - print(f"{RED}✗ Found duplicate IDs: {', '.join(duplicates)}{RESET}") - all_valid = False - - if all_valid: - print(f"{GREEN}✓ All cross-references are valid!{RESET}") - else: - print(f"{RED}✗ Found validation errors!{RESET}") - - return all_valid - - def generate_summary(self) -> None: - """Generate a summary of the loaded fixtures""" - print(f"\n{BLUE}=== Fixture Summary ==={RESET}") - print(f"Tenants: {len(self.tenant_data)}") - print(f"Users: {len(self.user_data)}") - print(f"Products: {len(self.product_data)}") - print(f"Suppliers: {len(self.supplier_data)}") - print(f"Recipes: {len(self.recipe_data)}") - print(f"Locations: {len(self.location_data)}") - - print(f"\nEntity Types: {list(self.all_ids.keys())}") - - for entity_type, ids in self.all_ids.items(): - print(f" {entity_type}: {len(ids)} IDs") - - print(f"\nReference Types: {list(self.references.keys())}") - for ref_type, refs in self.references.items(): - print(f" {ref_type}: {len(refs)} references") - - def run_validation(self) -> bool: - """Run the complete validation process""" - print(f"{BLUE}=== Enterprise Demo Fixtures Validator ==={RESET}") - print(f"Base Path: {self.base_path}\n") - - try: - self.load_all_fixtures() - self.generate_summary() - return self.validate_all_references() - - except Exception as e: - print(f"{RED}✗ Validation failed with error: {e}{RESET}") - import traceback - traceback.print_exc() - return False - -if __name__ == "__main__": - validator = FixtureValidator() - success = validator.run_validation() - - if success: - print(f"\n{GREEN}=== Validation Complete: All checks passed! ==={RESET}") - sys.exit(0) - else: - print(f"\n{RED}=== Validation Complete: Errors found! ==={RESET}") - sys.exit(1) diff --git a/tests/integration/test_tenant_deletion.py b/tests/integration/test_tenant_deletion.py deleted file mode 100644 index af9ba9bd..00000000 --- a/tests/integration/test_tenant_deletion.py +++ /dev/null @@ -1,377 +0,0 @@ -""" -Integration Tests for Tenant Deletion System -Tests the complete deletion flow across all 12 microservices -""" - -import asyncio -import pytest -import httpx -import os -from typing import Dict, List, Any -from uuid import uuid4 -from datetime import timedelta -import structlog - -from shared.auth.jwt_handler import JWTHandler - -logger = structlog.get_logger(__name__) - - -# Test Configuration -BASE_URLS = { - "tenant": "http://tenant-service:8000/api/v1", - "orders": "http://orders-service:8000/api/v1", - "inventory": "http://inventory-service:8000/api/v1", - "recipes": "http://recipes-service:8000/api/v1", - "sales": "http://sales-service:8000/api/v1", - "production": "http://production-service:8000/api/v1", - "suppliers": "http://suppliers-service:8000/api/v1", - "pos": "http://pos-service:8000/api/v1", - "external": "http://external-service:8000/api/v1", - "forecasting": "http://forecasting-service:8000/api/v1", - "training": "http://training-service:8000/api/v1", - "alert_processor": "http://alert-processor-service:8000/api/v1", - "notification": "http://notification-service:8000/api/v1", -} - -# Test tenant ID (use a real demo tenant from the system) -TEST_TENANT_ID = "dbc2128a-7539-470c-94b9-c1e37031bd77" # Demo tenant - - -@pytest.fixture -async def service_token(): - """Get a service JWT token for authentication""" - # Get JWT secret from environment or use default for testing - jwt_secret = os.getenv("JWT_SECRET", "test-secret-key-for-integration-tests-only") - - # Create JWT handler - jwt_handler = JWTHandler(secret_key=jwt_secret) - - # Generate service token with 1 hour expiration for tests - token = jwt_handler.create_service_token( - service_name="integration-test-service", - expires_delta=timedelta(hours=1) - ) - - logger.info("Generated service token for integration tests") - return token - - -@pytest.fixture -async def http_client(): - """Create async HTTP client""" - async with httpx.AsyncClient(verify=False, timeout=30.0) as client: - yield client - - -class TestIndividualServiceDeletion: - """Test each service's deletion endpoint individually""" - - @pytest.mark.asyncio - async def test_orders_service_preview(self, http_client, service_token): - """Test Orders service deletion preview""" - url = f"{BASE_URLS['orders']}/orders/tenant/{TEST_TENANT_ID}/deletion-preview" - headers = {"Authorization": f"Bearer {service_token}"} - - response = await http_client.get(url, headers=headers) - - assert response.status_code == 200 - data = response.json() - assert "preview" in data - assert "total_records" in data - assert data["service"] == "orders" - logger.info("orders.preview_test.passed", data=data) - - @pytest.mark.asyncio - async def test_inventory_service_preview(self, http_client, service_token): - """Test Inventory service deletion preview""" - url = f"{BASE_URLS['inventory']}/inventory/tenant/{TEST_TENANT_ID}/deletion-preview" - headers = {"Authorization": f"Bearer {service_token}"} - - response = await http_client.get(url, headers=headers) - - assert response.status_code == 200 - data = response.json() - assert "preview" in data - assert "total_records" in data - logger.info("inventory.preview_test.passed", data=data) - - @pytest.mark.asyncio - async def test_recipes_service_preview(self, http_client, service_token): - """Test Recipes service deletion preview""" - url = f"{BASE_URLS['recipes']}/recipes/tenant/{TEST_TENANT_ID}/deletion-preview" - headers = {"Authorization": f"Bearer {service_token}"} - - response = await http_client.get(url, headers=headers) - - assert response.status_code == 200 - data = response.json() - assert "preview" in data - logger.info("recipes.preview_test.passed", data=data) - - @pytest.mark.asyncio - async def test_forecasting_service_preview(self, http_client, service_token): - """Test Forecasting service deletion preview""" - url = f"{BASE_URLS['forecasting']}/forecasting/tenant/{TEST_TENANT_ID}/deletion-preview" - headers = {"Authorization": f"Bearer {service_token}"} - - response = await http_client.get(url, headers=headers) - - assert response.status_code == 200 - data = response.json() - assert "preview" in data - assert data["service"] == "forecasting" - logger.info("forecasting.preview_test.passed", data=data) - - @pytest.mark.asyncio - async def test_training_service_preview(self, http_client, service_token): - """Test Training service deletion preview""" - url = f"{BASE_URLS['training']}/training/tenant/{TEST_TENANT_ID}/deletion-preview" - headers = {"Authorization": f"Bearer {service_token}"} - - response = await http_client.get(url, headers=headers) - - assert response.status_code == 200 - data = response.json() - assert "preview" in data - assert data["service"] == "training" - logger.info("training.preview_test.passed", data=data) - - @pytest.mark.asyncio - async def test_notification_service_preview(self, http_client, service_token): - """Test Notification service deletion preview""" - url = f"{BASE_URLS['notification']}/notifications/tenant/{TEST_TENANT_ID}/deletion-preview" - headers = {"Authorization": f"Bearer {service_token}"} - - response = await http_client.get(url, headers=headers) - - assert response.status_code == 200 - data = response.json() - assert "preview" in data - assert data["service"] == "notification" - logger.info("notification.preview_test.passed", data=data) - - @pytest.mark.asyncio - async def test_all_services_preview_parallel(self, http_client, service_token): - """Test all services' preview endpoints in parallel""" - headers = {"Authorization": f"Bearer {service_token}"} - - # Define all preview URLs - preview_urls = { - "orders": f"{BASE_URLS['orders']}/orders/tenant/{TEST_TENANT_ID}/deletion-preview", - "inventory": f"{BASE_URLS['inventory']}/inventory/tenant/{TEST_TENANT_ID}/deletion-preview", - "recipes": f"{BASE_URLS['recipes']}/recipes/tenant/{TEST_TENANT_ID}/deletion-preview", - "sales": f"{BASE_URLS['sales']}/sales/tenant/{TEST_TENANT_ID}/deletion-preview", - "production": f"{BASE_URLS['production']}/production/tenant/{TEST_TENANT_ID}/deletion-preview", - "suppliers": f"{BASE_URLS['suppliers']}/suppliers/tenant/{TEST_TENANT_ID}/deletion-preview", - "pos": f"{BASE_URLS['pos']}/pos/tenant/{TEST_TENANT_ID}/deletion-preview", - "external": f"{BASE_URLS['external']}/external/tenant/{TEST_TENANT_ID}/deletion-preview", - "forecasting": f"{BASE_URLS['forecasting']}/forecasting/tenant/{TEST_TENANT_ID}/deletion-preview", - "training": f"{BASE_URLS['training']}/training/tenant/{TEST_TENANT_ID}/deletion-preview", - "alert_processor": f"{BASE_URLS['alert_processor']}/alerts/tenant/{TEST_TENANT_ID}/deletion-preview", - "notification": f"{BASE_URLS['notification']}/notifications/tenant/{TEST_TENANT_ID}/deletion-preview", - } - - # Make all requests in parallel - tasks = [ - http_client.get(url, headers=headers) - for url in preview_urls.values() - ] - - responses = await asyncio.gather(*tasks, return_exceptions=True) - - # Analyze results - results = {} - for service, response in zip(preview_urls.keys(), responses): - if isinstance(response, Exception): - results[service] = {"status": "error", "error": str(response)} - else: - results[service] = { - "status": "success" if response.status_code == 200 else "failed", - "status_code": response.status_code, - "data": response.json() if response.status_code == 200 else None - } - - # Log summary - successful = sum(1 for r in results.values() if r["status"] == "success") - logger.info("parallel_preview_test.completed", - total_services=len(results), - successful=successful, - failed=len(results) - successful, - results=results) - - # Assert at least 10 services responded successfully - assert successful >= 10, f"Only {successful}/12 services responded successfully" - - return results - - -class TestOrchestratedDeletion: - """Test the orchestrator's ability to delete across all services""" - - @pytest.mark.asyncio - async def test_orchestrator_preview_all_services(self, http_client, service_token): - """Test orchestrator can preview deletion across all services""" - from services.auth.app.services.deletion_orchestrator import DeletionOrchestrator - - orchestrator = DeletionOrchestrator(auth_token=service_token) - - # Get preview from all services - previews = {} - for service_name, endpoint_template in orchestrator.SERVICE_DELETION_ENDPOINTS.items(): - url = endpoint_template.format(tenant_id=TEST_TENANT_ID) + "/deletion-preview" - try: - response = await http_client.get( - url, - headers={"Authorization": f"Bearer {service_token}"}, - timeout=10.0 - ) - if response.status_code == 200: - previews[service_name] = response.json() - else: - previews[service_name] = {"error": f"HTTP {response.status_code}"} - except Exception as e: - previews[service_name] = {"error": str(e)} - - logger.info("orchestrator.preview_test.completed", - services_count=len(previews), - previews=previews) - - # Calculate total records across all services - total_records = 0 - for service, data in previews.items(): - if "total_records" in data: - total_records += data["total_records"] - - logger.info("orchestrator.preview_test.total_records", - total_records=total_records, - services=len(previews)) - - assert len(previews) == 12, "Should have previews from all 12 services" - assert total_records >= 0, "Should have valid record counts" - - -class TestErrorHandling: - """Test error handling and edge cases""" - - @pytest.mark.asyncio - async def test_invalid_tenant_id(self, http_client, service_token): - """Test deletion with invalid tenant ID""" - invalid_tenant_id = str(uuid4()) - url = f"{BASE_URLS['orders']}/orders/tenant/{invalid_tenant_id}/deletion-preview" - headers = {"Authorization": f"Bearer {service_token}"} - - response = await http_client.get(url, headers=headers) - - # Should succeed with zero counts for non-existent tenant - assert response.status_code == 200 - data = response.json() - assert data["total_records"] == 0 - - @pytest.mark.asyncio - async def test_unauthorized_access(self, http_client): - """Test deletion without authentication""" - url = f"{BASE_URLS['orders']}/orders/tenant/{TEST_TENANT_ID}/deletion-preview" - - response = await http_client.get(url) - - # Should be unauthorized - assert response.status_code in [401, 403] - - @pytest.mark.asyncio - async def test_service_timeout_handling(self, http_client, service_token): - """Test handling of service timeouts""" - # Use a very short timeout to simulate timeout - async with httpx.AsyncClient(verify=False, timeout=0.001) as short_client: - url = f"{BASE_URLS['orders']}/orders/tenant/{TEST_TENANT_ID}/deletion-preview" - headers = {"Authorization": f"Bearer {service_token}"} - - with pytest.raises((httpx.TimeoutException, httpx.ConnectTimeout)): - await short_client.get(url, headers=headers) - - -class TestDataIntegrity: - """Test data integrity after deletion""" - - @pytest.mark.asyncio - async def test_cascade_deletion_order(self, http_client, service_token): - """Test that child records are deleted before parents""" - # This would require creating test data and verifying deletion order - # For now, we verify the preview shows proper counts - - url = f"{BASE_URLS['orders']}/orders/tenant/{TEST_TENANT_ID}/deletion-preview" - headers = {"Authorization": f"Bearer {service_token}"} - - response = await http_client.get(url, headers=headers) - assert response.status_code == 200 - - data = response.json() - preview = data.get("preview", {}) - - # Verify we have counts for both parents and children - # In orders service: order_items (child) and orders (parent) - if preview.get("order_items", 0) > 0: - assert preview.get("orders", 0) > 0, "If items exist, orders should exist" - - logger.info("cascade_deletion_test.passed", preview=preview) - - -class TestPerformance: - """Test performance of deletion operations""" - - @pytest.mark.asyncio - async def test_parallel_deletion_performance(self, http_client, service_token): - """Test performance of parallel deletion across services""" - import time - - headers = {"Authorization": f"Bearer {service_token}"} - preview_urls = [ - f"{BASE_URLS['orders']}/orders/tenant/{TEST_TENANT_ID}/deletion-preview", - f"{BASE_URLS['inventory']}/inventory/tenant/{TEST_TENANT_ID}/deletion-preview", - f"{BASE_URLS['forecasting']}/forecasting/tenant/{TEST_TENANT_ID}/deletion-preview", - f"{BASE_URLS['training']}/training/tenant/{TEST_TENANT_ID}/deletion-preview", - ] - - # Test parallel execution - start_time = time.time() - tasks = [http_client.get(url, headers=headers) for url in preview_urls] - responses = await asyncio.gather(*tasks, return_exceptions=True) - parallel_duration = time.time() - start_time - - # Test sequential execution - start_time = time.time() - for url in preview_urls: - await http_client.get(url, headers=headers) - sequential_duration = time.time() - start_time - - logger.info("performance_test.completed", - parallel_duration=parallel_duration, - sequential_duration=sequential_duration, - speedup=sequential_duration / parallel_duration if parallel_duration > 0 else 0) - - # Parallel should be faster - assert parallel_duration < sequential_duration, "Parallel execution should be faster" - - -# Helper function to run all tests -async def run_all_tests(): - """Run all integration tests""" - import sys - - logger.info("integration_tests.starting") - - # Run pytest programmatically - exit_code = pytest.main([ - __file__, - "-v", - "-s", - "--tb=short", - "--log-cli-level=INFO" - ]) - - logger.info("integration_tests.completed", exit_code=exit_code) - sys.exit(exit_code) - - -if __name__ == "__main__": - asyncio.run(run_all_tests()) diff --git a/verify_fixes.sh b/verify_fixes.sh deleted file mode 100755 index f95c5af1..00000000 --- a/verify_fixes.sh +++ /dev/null @@ -1,111 +0,0 @@ -#!/bin/bash -# Verification Script for Demo Session Fixes -# Date: 2025-12-16 - -echo "==========================================" -echo "Demo Session & AI Insights Verification" -echo "==========================================" -echo "" - -# Color codes -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# 1. Verify Orchestrator Fix -echo "1. Checking Orchestrator Import Fix..." -if grep -q "OrchestrationRun, OrchestrationStatus" services/orchestrator/app/api/internal_demo.py; then - echo -e "${GREEN}✓ OrchestrationStatus import added${NC}" -else - echo -e "${RED}✗ OrchestrationStatus import missing${NC}" - exit 1 -fi -echo "" - -# 2. Check if AI insights data was generated -echo "2. Checking AI Insights Data in Fixtures..." - -# Check stock movements -STOCK_MOVEMENTS=$(cat shared/demo/fixtures/professional/03-inventory.json | jq '.stock_movements | length' 2>/dev/null) -if [ "$STOCK_MOVEMENTS" -gt 800 ]; then - echo -e "${GREEN}✓ Stock movements: $STOCK_MOVEMENTS (need 800+)${NC}" -else - echo -e "${YELLOW}⚠ Stock movements: $STOCK_MOVEMENTS (expected 800+)${NC}" - echo " Run: python shared/demo/fixtures/professional/generate_ai_insights_data.py" -fi - -# Check worker assignments -WORKERS=$(cat shared/demo/fixtures/professional/06-production.json | jq '[.batches[] | select(.staff_assigned != null)] | length' 2>/dev/null) -if [ "$WORKERS" -gt 200 ]; then - echo -e "${GREEN}✓ Worker assignments: $WORKERS (need 200+)${NC}" -else - echo -e "${YELLOW}⚠ Worker assignments: $WORKERS (expected 200+)${NC}" - echo " Run: python shared/demo/fixtures/professional/generate_ai_insights_data.py" -fi - -# Check stockout events -STOCKOUTS=$(cat shared/demo/fixtures/professional/03-inventory.json | jq '[.stock_movements[] | select(.quantity_after == 0.0)] | length' 2>/dev/null) -if [ "$STOCKOUTS" -ge 5 ]; then - echo -e "${GREEN}✓ Stockout events: $STOCKOUTS (need 5+)${NC}" -else - echo -e "${YELLOW}⚠ Stockout events: $STOCKOUTS (expected 5+)${NC}" - echo " Run: python shared/demo/fixtures/professional/generate_ai_insights_data.py" -fi -echo "" - -# 3. Check Kubernetes pods -echo "3. Checking Kubernetes Pods..." -kubectl get pods -n bakery-ia | grep -E "(orchestrator|ai-insights|demo-session)" | while read line; do - POD_NAME=$(echo $line | awk '{print $1}') - STATUS=$(echo $line | awk '{print $3}') - - if [[ "$STATUS" == "Running" ]]; then - echo -e "${GREEN}✓ $POD_NAME: $STATUS${NC}" - elif [[ "$STATUS" == "Completed" ]]; then - echo -e "${GREEN}✓ $POD_NAME: $STATUS${NC}" - else - echo -e "${RED}✗ $POD_NAME: $STATUS${NC}" - fi -done -echo "" - -# 4. Instructions -echo "==========================================" -echo "Next Steps:" -echo "==========================================" -echo "" -echo "1. Redeploy Orchestrator Service:" -echo " kubectl delete pod -n bakery-ia \$(kubectl get pods -n bakery-ia | grep orchestrator-service | awk '{print \$1}')" -echo "" -echo "2. Wait for new pod to be ready:" -echo " kubectl wait --for=condition=ready pod -l app=orchestrator-service -n bakery-ia --timeout=60s" -echo "" -echo "3. Create a new demo session:" -echo " curl -X POST http://localhost:8000/api/demo/sessions \\" -echo " -H \"Content-Type: application/json\" \\" -echo " -d '{\"demo_account_type\":\"professional\"}'" -echo "" -echo "4. Monitor cloning progress:" -echo " kubectl logs -n bakery-ia -f \$(kubectl get pods -n bakery-ia | grep demo-session-service | awk '{print \$1}') | grep -E 'orchestrator|AI insights'" -echo "" -echo "5. Verify AI insights generated:" -echo " # Wait 60 seconds after session ready, then check insights count" -echo " # Should see 5-10 insights if data was populated" -echo "" - -echo "==========================================" -echo "Troubleshooting:" -echo "==========================================" -echo "" -echo "If AI insights count is low (< 5):" -echo "1. Run the data generator:" -echo " python shared/demo/fixtures/professional/generate_ai_insights_data.py" -echo "" -echo "2. Create a new demo session" -echo "" -echo "3. Check service logs for ML model execution:" -echo " kubectl logs -n bakery-ia \$(kubectl get pods -n bakery-ia | grep inventory-service | awk '{print \$1}') | grep -i 'ai_insights\\|safety_stock'" -echo " kubectl logs -n bakery-ia \$(kubectl get pods -n bakery-ia | grep production-service | awk '{print \$1}') | grep -i 'ai_insights\\|yield'" -echo "" -