""" 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 logger = structlog.get_logger() router = APIRouter(prefix="/internal/demo", tags=["internal"]) # Internal API key for service-to-service auth INTERNAL_API_KEY = os.getenv("INTERNAL_API_KEY", "dev-internal-key-change-in-production") # Base demo tenant IDs DEMO_TENANT_SAN_PABLO = "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6" DEMO_TENANT_LA_ESPIGA = "b2c3d4e5-f6a7-48b9-c0d1-e2f3a4b5c6d7" 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 != 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 app.models.tenants import Subscription 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 = { "individual_bakery": "c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6", # María García López (San Pablo) "central_baker": "d2e3f4a5-b6c7-48d9-e0f1-a2b3c4d5e6f7" # Carlos Martínez Ruiz (La Espiga) } demo_owner_uuid = uuid.UUID(DEMO_OWNER_IDS.get(demo_account_type, DEMO_OWNER_IDS["individual_bakery"])) 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, business_model=demo_account_type, subscription_tier="demo", # Special tier for demo sessions 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 tenant member record for the demo owner from app.models.tenants import TenantMember import json tenant_member = TenantMember( tenant_id=virtual_uuid, user_id=demo_owner_uuid, role="owner", permissions=json.dumps(["read", "write", "admin"]), # Convert list to JSON string 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) # Clone subscription from template tenant from app.models.tenants import Subscription from datetime import timedelta # 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() subscription_plan = "unknown" 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) subscription_plan = subscription.plan logger.info( "Cloning subscription from template tenant", template_tenant_id=base_tenant_id, virtual_tenant_id=virtual_tenant_id, plan=subscription_plan ) else: logger.warning( "No subscription found on template tenant - virtual tenant will have no subscription", base_tenant_id=base_tenant_id ) 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 cloned subscription", virtual_tenant_id=virtual_tenant_id, tenant_name=tenant.name, subscription_plan=subscription_plan, duration_ms=duration_ms ) return { "service": "tenant", "status": "completed", "records_cloned": 3 if template_subscription else 2, # Tenant + TenantMember + Subscription (if found) "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), "member_created": True, "subscription_plan": subscription_plan, "subscription_cloned": template_subscription is not None } } 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.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" }