""" Internal Demo Cloning API Service-to-service endpoint for cloning tenant data """ from fastapi import APIRouter, Depends, HTTPException, Header from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select import structlog import uuid from datetime import datetime, timezone from typing import Optional import os from app.core.database import get_db from app.models.tenants import Tenant, Subscription, TenantMember from app.models.tenant_location import TenantLocation from app.core.config import settings logger = structlog.get_logger() router = APIRouter(prefix="/internal/demo", tags=["internal"]) # Base demo tenant IDs DEMO_TENANT_PROFESSIONAL = "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6" def verify_internal_api_key(x_internal_api_key: Optional[str] = Header(None)): """Verify internal API key for service-to-service communication""" if x_internal_api_key != settings.INTERNAL_API_KEY: logger.warning("Unauthorized internal API access attempted") raise HTTPException(status_code=403, detail="Invalid internal API key") return True @router.post("/clone") async def clone_demo_data( base_tenant_id: str, virtual_tenant_id: str, demo_account_type: str, session_id: Optional[str] = None, db: AsyncSession = Depends(get_db), _: bool = Depends(verify_internal_api_key) ): """ Clone tenant service data for a virtual demo tenant This endpoint creates the virtual tenant record that will be used for the demo session. No actual data cloning is needed in tenant service beyond creating the tenant record itself. Args: base_tenant_id: Template tenant UUID (not used, for consistency) virtual_tenant_id: Target virtual tenant UUID demo_account_type: Type of demo account session_id: Originating session ID for tracing Returns: Cloning status and record count """ start_time = datetime.now(timezone.utc) logger.info( "Starting tenant data cloning", virtual_tenant_id=virtual_tenant_id, demo_account_type=demo_account_type, session_id=session_id ) try: # Validate UUIDs virtual_uuid = uuid.UUID(virtual_tenant_id) # Check if tenant already exists result = await db.execute( select(Tenant).where(Tenant.id == virtual_uuid) ) existing_tenant = result.scalars().first() if existing_tenant: logger.info( "Virtual tenant already exists", virtual_tenant_id=virtual_tenant_id, tenant_name=existing_tenant.name ) # Ensure the tenant has a subscription (copy from template if missing) from datetime import timedelta result = await db.execute( select(Subscription).where( Subscription.tenant_id == virtual_uuid, Subscription.status == "active" ) ) existing_subscription = result.scalars().first() if not existing_subscription: logger.info("Creating missing subscription for existing virtual tenant by copying from template", virtual_tenant_id=virtual_tenant_id, base_tenant_id=base_tenant_id) # Get subscription from template tenant base_uuid = uuid.UUID(base_tenant_id) result = await db.execute( select(Subscription).where( Subscription.tenant_id == base_uuid, Subscription.status == "active" ) ) template_subscription = result.scalars().first() if template_subscription: # Clone subscription from template subscription = Subscription( tenant_id=virtual_uuid, plan=template_subscription.plan, status=template_subscription.status, monthly_price=template_subscription.monthly_price, max_users=template_subscription.max_users, max_locations=template_subscription.max_locations, max_products=template_subscription.max_products, features=template_subscription.features.copy() if template_subscription.features else {}, trial_ends_at=template_subscription.trial_ends_at, next_billing_date=datetime.now(timezone.utc) + timedelta(days=90) if template_subscription.next_billing_date else None ) db.add(subscription) await db.commit() logger.info("Subscription cloned successfully", virtual_tenant_id=virtual_tenant_id, plan=subscription.plan) else: logger.warning("No subscription found on template tenant", base_tenant_id=base_tenant_id) # Return success - idempotent operation duration_ms = int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000) return { "service": "tenant", "status": "completed", "records_cloned": 0 if existing_subscription else 1, "duration_ms": duration_ms, "details": { "tenant_already_exists": True, "tenant_id": str(virtual_uuid), "subscription_created": not existing_subscription } } # Create virtual tenant record with required fields # Note: Use the actual demo user IDs from seed_demo_users.py # These match the demo users created in the auth service DEMO_OWNER_IDS = { "professional": "c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6", # María García López "enterprise": "d2e3f4a5-b6c7-48d9-e0f1-a2b3c4d5e6f7" # Carlos Martínez Ruiz } demo_owner_uuid = uuid.UUID(DEMO_OWNER_IDS.get(demo_account_type, DEMO_OWNER_IDS["professional"])) tenant = Tenant( id=virtual_uuid, name=f"Demo Tenant - {demo_account_type.replace('_', ' ').title()}", address="Calle Demo 123", # Required field - provide demo address city="Madrid", postal_code="28001", business_type="bakery", is_demo=True, is_demo_template=False, demo_session_id=session_id, # Link tenant to demo session business_model=demo_account_type, is_active=True, timezone="Europe/Madrid", owner_id=demo_owner_uuid # Required field - matches seed_demo_users.py ) db.add(tenant) await db.flush() # Flush to get the tenant ID # Create demo subscription with appropriate tier based on demo account type # Determine subscription tier based on demo account type if demo_account_type == "professional": plan = "professional" max_locations = 3 elif demo_account_type in ["enterprise", "enterprise_parent"]: plan = "enterprise" max_locations = -1 # Unlimited elif demo_account_type == "enterprise_child": plan = "enterprise" max_locations = 1 else: plan = "starter" max_locations = 1 demo_subscription = Subscription( tenant_id=tenant.id, plan=plan, # Set appropriate tier based on demo account type status="active", monthly_price=0.0, # Free for demo billing_cycle="monthly", max_users=-1, # Unlimited for demo max_locations=max_locations, max_products=-1, # Unlimited for demo features={} ) db.add(demo_subscription) # Create tenant member records for demo owner and staff import json # Helper function to get permissions for role def get_permissions_for_role(role: str) -> str: permission_map = { "owner": ["read", "write", "admin", "delete"], "admin": ["read", "write", "admin"], "production_manager": ["read", "write"], "baker": ["read", "write"], "sales": ["read", "write"], "quality_control": ["read", "write"], "warehouse": ["read", "write"], "logistics": ["read", "write"], "procurement": ["read", "write"], "maintenance": ["read", "write"], "member": ["read", "write"], "viewer": ["read"] } permissions = permission_map.get(role, ["read"]) return json.dumps(permissions) # Define staff users for each demo account type (must match seed_demo_tenant_members.py) STAFF_USERS = { "professional": [ # Owner { "user_id": uuid.UUID("c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6"), "role": "owner" }, # Staff { "user_id": uuid.UUID("50000000-0000-0000-0000-000000000001"), "role": "baker" }, { "user_id": uuid.UUID("50000000-0000-0000-0000-000000000002"), "role": "sales" }, { "user_id": uuid.UUID("50000000-0000-0000-0000-000000000003"), "role": "quality_control" }, { "user_id": uuid.UUID("50000000-0000-0000-0000-000000000004"), "role": "admin" }, { "user_id": uuid.UUID("50000000-0000-0000-0000-000000000005"), "role": "warehouse" }, { "user_id": uuid.UUID("50000000-0000-0000-0000-000000000006"), "role": "production_manager" } ], "enterprise": [ # Owner { "user_id": uuid.UUID("d2e3f4a5-b6c7-48d9-e0f1-a2b3c4d5e6f7"), "role": "owner" }, # Staff { "user_id": uuid.UUID("50000000-0000-0000-0000-000000000011"), "role": "production_manager" }, { "user_id": uuid.UUID("50000000-0000-0000-0000-000000000012"), "role": "quality_control" }, { "user_id": uuid.UUID("50000000-0000-0000-0000-000000000013"), "role": "logistics" }, { "user_id": uuid.UUID("50000000-0000-0000-0000-000000000014"), "role": "sales" }, { "user_id": uuid.UUID("50000000-0000-0000-0000-000000000015"), "role": "procurement" }, { "user_id": uuid.UUID("50000000-0000-0000-0000-000000000016"), "role": "maintenance" } ] } # Get staff users for this demo account type staff_users = STAFF_USERS.get(demo_account_type, []) # Create tenant member records for all users (owner + staff) members_created = 0 for staff_member in staff_users: tenant_member = TenantMember( tenant_id=virtual_uuid, user_id=staff_member["user_id"], role=staff_member["role"], permissions=get_permissions_for_role(staff_member["role"]), is_active=True, invited_by=demo_owner_uuid, invited_at=datetime.now(timezone.utc), joined_at=datetime.now(timezone.utc), created_at=datetime.now(timezone.utc) ) db.add(tenant_member) members_created += 1 logger.info( "Created tenant members for virtual tenant", virtual_tenant_id=virtual_tenant_id, members_created=members_created ) # Clone TenantLocations from app.models.tenant_location import TenantLocation base_uuid = uuid.UUID(base_tenant_id) location_result = await db.execute( select(TenantLocation).where(TenantLocation.tenant_id == base_uuid) ) base_locations = location_result.scalars().all() records_cloned = 1 + members_created # Tenant + TenantMembers for base_location in base_locations: virtual_location = TenantLocation( id=uuid.uuid4(), tenant_id=virtual_tenant_id, name=base_location.name, location_type=base_location.location_type, address=base_location.address, city=base_location.city, postal_code=base_location.postal_code, latitude=base_location.latitude, longitude=base_location.longitude, capacity=base_location.capacity, delivery_windows=base_location.delivery_windows, operational_hours=base_location.operational_hours, max_delivery_radius_km=base_location.max_delivery_radius_km, delivery_schedule_config=base_location.delivery_schedule_config, is_active=base_location.is_active, contact_person=base_location.contact_person, contact_phone=base_location.contact_phone, contact_email=base_location.contact_email, metadata_=base_location.metadata_ if isinstance(base_location.metadata_, dict) else (base_location.metadata_ or {}) ) db.add(virtual_location) records_cloned += 1 logger.info("Cloned TenantLocations", count=len(base_locations)) # Subscription already created earlier based on demo_account_type (lines 179-206) # No need to clone from template - this prevents duplicate subscription creation await db.commit() await db.refresh(tenant) duration_ms = int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000) logger.info( "Virtual tenant created successfully with subscription", virtual_tenant_id=virtual_tenant_id, tenant_name=tenant.name, subscription_plan=plan, duration_ms=duration_ms ) records_cloned = 1 + members_created + 1 # Tenant + TenantMembers + Subscription return { "service": "tenant", "status": "completed", "records_cloned": records_cloned, "duration_ms": duration_ms, "details": { "tenant_id": str(tenant.id), "tenant_name": tenant.name, "business_model": tenant.business_model, "owner_id": str(demo_owner_uuid), "members_created": members_created, "subscription_plan": plan, "subscription_created": True } } except ValueError as e: logger.error("Invalid UUID format", error=str(e), virtual_tenant_id=virtual_tenant_id) raise HTTPException(status_code=400, detail=f"Invalid UUID: {str(e)}") except Exception as e: logger.error( "Failed to clone tenant data", error=str(e), virtual_tenant_id=virtual_tenant_id, exc_info=True ) # Rollback on error await db.rollback() return { "service": "tenant", "status": "failed", "records_cloned": 0, "duration_ms": int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000), "error": str(e) } @router.post("/create-child") async def create_child_outlet( request: dict, db: AsyncSession = Depends(get_db), _: bool = Depends(verify_internal_api_key) ): """ Create a child outlet tenant for enterprise demos Args: request: JSON request body with child tenant details Returns: Creation status and tenant details """ # Extract parameters from request body base_tenant_id = request.get("base_tenant_id") virtual_tenant_id = request.get("virtual_tenant_id") parent_tenant_id = request.get("parent_tenant_id") child_name = request.get("child_name") location = request.get("location", {}) session_id = request.get("session_id") start_time = datetime.now(timezone.utc) logger.info( "Creating child outlet tenant", virtual_tenant_id=virtual_tenant_id, parent_tenant_id=parent_tenant_id, child_name=child_name, session_id=session_id ) try: # Validate UUIDs virtual_uuid = uuid.UUID(virtual_tenant_id) parent_uuid = uuid.UUID(parent_tenant_id) # Check if child tenant already exists result = await db.execute(select(Tenant).where(Tenant.id == virtual_uuid)) existing_tenant = result.scalars().first() if existing_tenant: logger.info( "Child tenant already exists", virtual_tenant_id=virtual_tenant_id, tenant_name=existing_tenant.name ) # Return existing tenant - idempotent operation duration_ms = int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000) return { "service": "tenant", "status": "completed", "records_created": 0, "duration_ms": duration_ms, "details": { "tenant_id": str(virtual_uuid), "tenant_name": existing_tenant.name, "already_exists": True } } # Create child tenant with parent relationship child_tenant = Tenant( id=virtual_uuid, name=child_name, address=location.get("address", f"Calle Outlet {location.get('city', 'Madrid')}"), city=location.get("city", "Madrid"), postal_code=location.get("postal_code", "28001"), business_type="bakery", is_demo=True, is_demo_template=False, demo_session_id=session_id, # Link child tenant to demo session business_model="retail_outlet", is_active=True, timezone="Europe/Madrid", # Set parent relationship parent_tenant_id=parent_uuid, tenant_type="child", hierarchy_path=f"{str(parent_uuid)}.{str(virtual_uuid)}", # Owner ID - using demo owner ID from parent # In real implementation, this would be the same owner as the parent tenant owner_id=uuid.UUID("c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6") # Demo owner ID ) db.add(child_tenant) await db.flush() # Flush to get the tenant ID # Create TenantLocation for this retail outlet child_location = TenantLocation( id=uuid.uuid4(), tenant_id=virtual_uuid, name=f"{child_name} - Retail Outlet", location_type="retail_outlet", address=location.get("address", f"Calle Outlet {location.get('city', 'Madrid')}"), city=location.get("city", "Madrid"), postal_code=location.get("postal_code", "28001"), latitude=location.get("latitude"), longitude=location.get("longitude"), delivery_windows={ "monday": "07:00-10:00", "wednesday": "07:00-10:00", "friday": "07:00-10:00" }, operational_hours={ "monday": "07:00-21:00", "tuesday": "07:00-21:00", "wednesday": "07:00-21:00", "thursday": "07:00-21:00", "friday": "07:00-21:00", "saturday": "08:00-21:00", "sunday": "09:00-21:00" }, delivery_schedule_config={ "delivery_days": ["monday", "wednesday", "friday"], "time_window": "07:00-10:00" }, is_active=True ) db.add(child_location) logger.info("Created TenantLocation for child", child_id=str(virtual_uuid), location_name=child_location.name) # Create parent tenant lookup to get the correct plan for the child parent_result = await db.execute( select(Subscription).where( Subscription.tenant_id == parent_uuid, Subscription.status == "active" ) ) parent_subscription = parent_result.scalars().first() # Child inherits the same plan as parent parent_plan = parent_subscription.plan if parent_subscription else "enterprise" child_subscription = Subscription( tenant_id=child_tenant.id, plan=parent_plan, # Child inherits the same plan as parent status="active", monthly_price=0.0, # Free for demo billing_cycle="monthly", max_users=10, # Demo limits max_locations=1, # Single location for outlet max_products=200, features={} ) db.add(child_subscription) # Create basic tenant members like parent import json # Demo owner is the same as central_baker/enterprise_chain owner (not individual_bakery) demo_owner_uuid = uuid.UUID("d2e3f4a5-b6c7-48d9-e0f1-a2b3c4d5e6f7") # Create tenant member for owner child_owner_member = TenantMember( tenant_id=virtual_uuid, user_id=demo_owner_uuid, role="owner", permissions=json.dumps(["read", "write", "admin", "delete"]), is_active=True, invited_by=demo_owner_uuid, invited_at=datetime.now(timezone.utc), joined_at=datetime.now(timezone.utc), created_at=datetime.now(timezone.utc) ) db.add(child_owner_member) # Create some staff members for the outlet (simplified) staff_users = [ { "user_id": uuid.UUID("50000000-0000-0000-0000-000000000002"), # Sales user "role": "sales" }, { "user_id": uuid.UUID("50000000-0000-0000-0000-000000000003"), # Quality control user "role": "quality_control" }, { "user_id": uuid.UUID("50000000-0000-0000-0000-000000000005"), # Warehouse user "role": "warehouse" } ] members_created = 1 # Start with owner for staff_member in staff_users: tenant_member = TenantMember( tenant_id=virtual_uuid, user_id=staff_member["user_id"], role=staff_member["role"], permissions=json.dumps(["read", "write"]) if staff_member["role"] != "admin" else json.dumps(["read", "write", "admin"]), is_active=True, invited_by=demo_owner_uuid, invited_at=datetime.now(timezone.utc), joined_at=datetime.now(timezone.utc), created_at=datetime.now(timezone.utc) ) db.add(tenant_member) members_created += 1 await db.commit() await db.refresh(child_tenant) duration_ms = int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000) logger.info( "Child outlet created successfully", virtual_tenant_id=str(virtual_tenant_id), parent_tenant_id=str(parent_tenant_id), child_name=child_name, duration_ms=duration_ms ) return { "service": "tenant", "status": "completed", "records_created": 2 + members_created, # Tenant + Subscription + Members "duration_ms": duration_ms, "details": { "tenant_id": str(child_tenant.id), "tenant_name": child_tenant.name, "parent_tenant_id": str(parent_tenant_id), "location": location, "members_created": members_created, "subscription_plan": "enterprise" } } except ValueError as e: logger.error("Invalid UUID format", error=str(e), virtual_tenant_id=virtual_tenant_id) raise HTTPException(status_code=400, detail=f"Invalid UUID: {str(e)}") except Exception as e: logger.error( "Failed to create child outlet", error=str(e), virtual_tenant_id=virtual_tenant_id, parent_tenant_id=parent_tenant_id, exc_info=True ) # Rollback on error await db.rollback() return { "service": "tenant", "status": "failed", "records_created": 0, "duration_ms": int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000), "error": str(e) } @router.get("/clone/health") async def clone_health_check(_: bool = Depends(verify_internal_api_key)): """ Health check for internal cloning endpoint Used by orchestrator to verify service availability """ return { "service": "tenant", "clone_endpoint": "available", "version": "2.0.0" }