"""Server demo operations.""" from database_service import database_connection, dict_from_row from kubernetes import client from kubernetes_service import k8s_manager from runtime_config import ( DATABASE_URL, logger, ) from server_capacity import calculate_server_cost from typing import Dict from user_service import add_credit_transaction, get_user_by_id, is_demo_enabled, update_user_credits import uuid __all__ = ['check_demo_eligibility', 'cleanup_expired_demo_servers', 'convert_demo_to_paid', 'mark_demo_used'] def check_demo_eligibility(user_id: str) -> Dict: """Check if user is eligible for demo server""" try: if not is_demo_enabled(): return { 'eligible': False, 'has_used_demo': False, 'has_purchased_credits': False, 'reason': 'DEMO servers are currently disabled' } with database_connection() as conn: cursor = conn.cursor() # Check if user has used demo before cursor.execute('SELECT id FROM demo_servers_used WHERE user_id = ?', (user_id,)) has_used_demo = cursor.fetchone() is not None # Check if user has purchased credits (type='purchase') cursor.execute(''' SELECT COUNT(*) FROM credit_transactions WHERE user_id = ? AND type = 'purchase' ''', (user_id,)) purchase_count = cursor.fetchone()['count'] has_purchased_credits = purchase_count > 0 eligible = not has_used_demo and not has_purchased_credits return { 'eligible': eligible, 'has_used_demo': has_used_demo, 'has_purchased_credits': has_purchased_credits, 'reason': None if eligible else ( 'You have already used the demo server' if has_used_demo else 'You have purchased credits before' ) } except Exception as e: logger.error(f"Error checking demo eligibility: {e}", exc_info=True) return {'eligible': False, 'has_used_demo': False, 'has_purchased_credits': False, 'reason': 'Error checking eligibility'} def mark_demo_used(user_id: str) -> bool: """Mark that user has used demo server""" try: with database_connection() as conn: cursor = conn.cursor() demo_id = f"demo-{str(uuid.uuid4())[:8]}" # PostgreSQL-compatible syntax if 'postgres' in DATABASE_URL: cursor.execute(''' INSERT INTO demo_servers_used (id, user_id) VALUES (%s, %s) ON CONFLICT (user_id) DO NOTHING ''', (demo_id, user_id)) else: # SQLite syntax cursor.execute(''' INSERT OR IGNORE INTO demo_servers_used (id, user_id) VALUES (?, ?) ''', (demo_id, user_id)) conn.commit() return True except Exception as e: logger.error(f"Error marking demo as used: {e}") return False def convert_demo_to_paid(server_id: str, user_id: str) -> Dict: """Convert demo server to paid server""" try: with database_connection() as conn: cursor = conn.cursor() # Get server details cursor.execute(''' SELECT * FROM servers WHERE id = ? AND owner_id = ? ''', (server_id, user_id)) server = dict_from_row(cursor.fetchone()) if not server: raise Exception('Server not found') if not server.get('is_demo'): raise Exception('Server is not a demo server') # Calculate server cost config = { 'serverType': server['type'], 'cpu': server['cpu'], 'ram': server['ram'], 'storage': server['storage'] } server_cost = calculate_server_cost(config) # Check user credits user = get_user_by_id(user_id) if user.get('credits', 0) < server_cost: raise Exception(f'Insufficient credits. Need {server_cost}, have {user.get("credits", 0)}') # Deduct credits new_credits = user.get('credits', 0) - server_cost if not update_user_credits(user_id, new_credits): raise Exception('Failed to deduct credits') add_credit_transaction(user_id, -server_cost, 'deduct', f'Convert demo server to paid: {server["name"]}') # Update server to remove demo status cursor.execute(''' UPDATE servers SET is_demo = FALSE, demo_expires_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ? ''', (server_id,)) conn.commit() logger.info(f"✓ Converted demo server {server_id} to paid server") return { 'success': True, 'server_id': server_id, 'credits_charged': server_cost, 'new_balance': new_credits } except Exception as e: logger.error(f"Error converting demo to paid: {e}") raise Exception(f'Failed to convert demo server: {str(e)}') def cleanup_expired_demo_servers(): """Delete expired demo servers""" try: with database_connection() as conn: cursor = conn.cursor() # Find expired demo servers cursor.execute(''' SELECT id, name, owner_id, namespace, type, k8s_name FROM servers WHERE is_demo = TRUE AND demo_expires_at IS NOT NULL AND demo_expires_at < CURRENT_TIMESTAMP ''') expired_servers = cursor.fetchall() deleted_count = 0 for row in expired_servers: server_id = row[0] server_name = row[1] owner_id = row[2] namespace = row[3] if len(row) > 3 else None server_type = row[4] if len(row) > 4 else 'vps' k8s_name = row[5] if len(row) > 5 else server_id # Use k8s_name if available, fallback to server_id try: # Delete from Kubernetes first if connected if k8s_manager.connected and namespace: try: logger.info(f"Attempting to delete Kubernetes resources for expired demo server {server_id} (k8s_name: {k8s_name}) in namespace {namespace}") # Import here to avoid issues if k8s not connected from kubernetes.client.rest import ApiException apps_v1 = client.AppsV1Api() v1 = client.CoreV1Api() # Try to delete deployment using k8s_name try: apps_v1.delete_namespaced_deployment(name=k8s_name, namespace=namespace) logger.info(f"✓ Deleted deployment for demo server {server_id} (k8s_name: {k8s_name})") except ApiException as e: if e.status != 404: # 404 means it doesn't exist, which is fine logger.warning(f"Could not delete deployment for {server_id} (k8s_name: {k8s_name}): {e}") # Try to delete service try: v1.delete_namespaced_service(name=f"{k8s_name}-service", namespace=namespace) logger.info(f"✓ Deleted service for demo server {server_id} (k8s_name: {k8s_name})") except ApiException as e: if e.status != 404: logger.warning(f"Could not delete service for {server_id} (k8s_name: {k8s_name}): {e}") except Exception as k8s_error: logger.error(f"Error deleting Kubernetes resources for {server_id}: {k8s_error}") # Delete server from database (this will cascade delete related records) cursor.execute('DELETE FROM servers WHERE id = ?', (server_id,)) conn.commit() deleted_count += 1 logger.info(f"✓ Deleted expired demo server: {server_name} ({server_id})") except Exception as e: logger.error(f"Error deleting expired demo server {server_id}: {e}") if deleted_count > 0: logger.info(f"✓ Cleaned up {deleted_count} expired demo server(s)") return deleted_count except Exception as e: logger.error(f"Error cleaning up expired demo servers: {e}") return 0