""" Internal Demo API Endpoints for POS Service Used by demo_session service to clone data for virtual demo tenants """ from fastapi import APIRouter, Depends, HTTPException, Header from typing import Dict, Any from uuid import UUID import structlog import os from app.core.database import get_db from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, delete, func from app.models.pos_config import POSConfiguration from app.models.pos_transaction import POSTransaction, POSTransactionItem import uuid from datetime import datetime, timezone from typing import Optional router = APIRouter() logger = structlog.get_logger() # Internal API key for service-to-service communication INTERNAL_API_KEY = os.getenv("INTERNAL_API_KEY", "dev-internal-key-change-in-production") def verify_internal_api_key(x_internal_api_key: str = Header(...)): """Verify internal API key for service-to-service communication""" if x_internal_api_key != INTERNAL_API_KEY: raise HTTPException(status_code=403, detail="Invalid internal API key") return True @router.post("/internal/demo/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 POS demo data from base tenant to virtual tenant This endpoint is called by the demo_session service during session initialization. It clones POS configurations and recent transactions. """ start_time = datetime.now(timezone.utc) session_created_at = datetime.now(timezone.utc) logger.info( "Starting POS data cloning with date adjustment", base_tenant_id=base_tenant_id, virtual_tenant_id=virtual_tenant_id, demo_account_type=demo_account_type, session_id=session_id, session_created_at=session_created_at.isoformat() ) try: base_uuid = uuid.UUID(base_tenant_id) virtual_uuid = uuid.UUID(virtual_tenant_id) # Fetch base tenant POS configurations result = await db.execute( select(POSConfiguration).where(POSConfiguration.tenant_id == base_uuid) ) base_configs = list(result.scalars().all()) configs_cloned = 0 transactions_cloned = 0 # Clone each configuration for base_config in base_configs: # Create new config for virtual tenant new_config = POSConfiguration( id=uuid.uuid4(), tenant_id=virtual_uuid, pos_system=base_config.pos_system, provider_name=f"{base_config.provider_name} (Demo Session)", is_active=base_config.is_active, is_connected=base_config.is_connected, encrypted_credentials=base_config.encrypted_credentials, webhook_url=base_config.webhook_url, webhook_secret=base_config.webhook_secret, environment=base_config.environment, location_id=base_config.location_id, merchant_id=base_config.merchant_id, sync_enabled=base_config.sync_enabled, sync_interval_minutes=base_config.sync_interval_minutes, auto_sync_products=base_config.auto_sync_products, auto_sync_transactions=base_config.auto_sync_transactions, last_sync_at=base_config.last_sync_at, last_successful_sync_at=base_config.last_successful_sync_at, last_sync_status=base_config.last_sync_status, last_sync_message=base_config.last_sync_message, provider_settings=base_config.provider_settings, last_health_check_at=base_config.last_health_check_at, health_status=base_config.health_status, health_message=base_config.health_message, created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), notes=f"Cloned from base config {base_config.id} for demo session {session_id}" ) db.add(new_config) await db.flush() configs_cloned += 1 # Clone recent transactions for this config tx_result = await db.execute( select(POSTransaction) .where(POSTransaction.pos_config_id == base_config.id) .order_by(POSTransaction.transaction_date.desc()) .limit(10) # Clone last 10 transactions ) base_transactions = list(tx_result.scalars().all()) # Clone each transaction for base_tx in base_transactions: new_tx = POSTransaction( id=uuid.uuid4(), tenant_id=virtual_uuid, pos_config_id=new_config.id, pos_system=base_tx.pos_system, external_transaction_id=base_tx.external_transaction_id, external_order_id=base_tx.external_order_id, transaction_type=base_tx.transaction_type, status=base_tx.status, subtotal=base_tx.subtotal, tax_amount=base_tx.tax_amount, tip_amount=base_tx.tip_amount, discount_amount=base_tx.discount_amount, total_amount=base_tx.total_amount, currency=base_tx.currency, payment_method=base_tx.payment_method, payment_status=base_tx.payment_status, transaction_date=base_tx.transaction_date, pos_created_at=base_tx.pos_created_at, pos_updated_at=base_tx.pos_updated_at, location_id=base_tx.location_id, location_name=base_tx.location_name, staff_id=base_tx.staff_id, staff_name=base_tx.staff_name, customer_id=base_tx.customer_id, customer_email=base_tx.customer_email, customer_phone=base_tx.customer_phone, order_type=base_tx.order_type, table_number=base_tx.table_number, receipt_number=base_tx.receipt_number, is_synced_to_sales=base_tx.is_synced_to_sales, sales_record_id=base_tx.sales_record_id, sync_attempted_at=base_tx.sync_attempted_at, sync_completed_at=base_tx.sync_completed_at, sync_error=base_tx.sync_error, sync_retry_count=base_tx.sync_retry_count, raw_data=base_tx.raw_data, is_processed=base_tx.is_processed, processing_error=base_tx.processing_error, is_duplicate=base_tx.is_duplicate, duplicate_of=base_tx.duplicate_of, created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc) ) db.add(new_tx) await db.flush() transactions_cloned += 1 # Clone transaction items item_result = await db.execute( select(POSTransactionItem).where(POSTransactionItem.transaction_id == base_tx.id) ) base_items = list(item_result.scalars().all()) for base_item in base_items: new_item = POSTransactionItem( id=uuid.uuid4(), transaction_id=new_tx.id, tenant_id=virtual_uuid, external_item_id=base_item.external_item_id, sku=base_item.sku, product_name=base_item.product_name, product_category=base_item.product_category, product_subcategory=base_item.product_subcategory, quantity=base_item.quantity, unit_price=base_item.unit_price, total_price=base_item.total_price, discount_amount=base_item.discount_amount, tax_amount=base_item.tax_amount, modifiers=base_item.modifiers, inventory_product_id=base_item.inventory_product_id, is_mapped_to_inventory=base_item.is_mapped_to_inventory, is_synced_to_sales=base_item.is_synced_to_sales, sync_error=base_item.sync_error, raw_data=base_item.raw_data, created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc) ) db.add(new_item) await db.commit() logger.info( "POS demo data cloned successfully", virtual_tenant_id=str(virtual_tenant_id), configs_cloned=configs_cloned, transactions_cloned=transactions_cloned ) return { "success": True, "records_cloned": configs_cloned + transactions_cloned, "configs_cloned": configs_cloned, "transactions_cloned": transactions_cloned, "service": "pos" } except Exception as e: logger.error("Failed to clone POS demo data", error=str(e), exc_info=True) await db.rollback() raise HTTPException(status_code=500, detail=f"Failed to clone POS demo data: {str(e)}") @router.delete("/internal/demo/tenant/{virtual_tenant_id}") async def delete_demo_data( virtual_tenant_id: str, db: AsyncSession = Depends(get_db), _: bool = Depends(verify_internal_api_key) ): """Delete all POS data for a virtual demo tenant""" logger.info("Deleting POS data for virtual tenant", virtual_tenant_id=virtual_tenant_id) start_time = datetime.now(timezone.utc) try: virtual_uuid = uuid.UUID(virtual_tenant_id) # Count records config_count = await db.scalar(select(func.count(POSConfiguration.id)).where(POSConfiguration.tenant_id == virtual_uuid)) transaction_count = await db.scalar(select(func.count(POSTransaction.id)).where(POSTransaction.tenant_id == virtual_uuid)) item_count = await db.scalar(select(func.count(POSTransactionItem.id)).where(POSTransactionItem.tenant_id == virtual_uuid)) # Delete in order (items -> transactions -> configs) await db.execute(delete(POSTransactionItem).where(POSTransactionItem.tenant_id == virtual_uuid)) await db.execute(delete(POSTransaction).where(POSTransaction.tenant_id == virtual_uuid)) await db.execute(delete(POSConfiguration).where(POSConfiguration.tenant_id == virtual_uuid)) await db.commit() duration_ms = int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000) logger.info("POS data deleted successfully", virtual_tenant_id=virtual_tenant_id, duration_ms=duration_ms) return { "service": "pos", "status": "deleted", "virtual_tenant_id": virtual_tenant_id, "records_deleted": { "configurations": config_count, "transactions": transaction_count, "items": item_count, "total": config_count + transaction_count + item_count }, "duration_ms": duration_ms } except Exception as e: logger.error("Failed to delete POS data", error=str(e), exc_info=True) await db.rollback() raise HTTPException(status_code=500, detail=str(e))