216 lines
8.8 KiB
Python
216 lines
8.8 KiB
Python
"""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
|