"""Server billing operations.""" from database_service import database_connection, dict_from_row from datetime import datetime from email_service import get_email_service import json from runtime_config import ( logger, ) import schedule from server_capacity import calculate_monthly_server_cost from server_lifecycle import stop_server import threading import time from typing import Dict, List, Optional from user_service import add_credit_transaction, update_user_credits import uuid __all__ = ['calculate_already_paid_this_month', 'charge_monthly_fees', 'check_and_charge_fees', 'get_active_servers', 'get_server_billing_info', 'get_user_credit_history', 'get_user_monthly_fees', 'save_fee_collection_log', 'save_monthly_fee_record', 'start_monthly_fee_scheduler'] def get_server_billing_info(server_id: str) -> Optional[Dict]: """Get billing information for a server""" try: with database_connection() as conn: cursor = conn.cursor() cursor.execute(''' SELECT * FROM server_billing WHERE server_id = ? ''', (server_id,)) return dict_from_row(cursor.fetchone()) except Exception as e: logger.error(f"Error getting server billing info: {e}") return None def calculate_already_paid_this_month(server_id: str, current_monthly_cost: int) -> int: """Calculate what the user has already paid this month based on days since last billing""" try: billing_info = get_server_billing_info(server_id) if not billing_info: return 0 # Use last_billing_date if available, otherwise use creation_date billing_date_str = billing_info.get('last_billing_date') or billing_info.get('creation_date') if not billing_date_str: return 0 billing_date = datetime.strptime(billing_date_str, '%Y-%m-%d').date() today = datetime.now().date() # Calculate days since last billing days_since_billing = (today - billing_date).days # If negative or zero, user hasn't been billed yet this cycle if days_since_billing <= 0: return 0 # Calculate days until next billing next_billing_date_str = billing_info.get('next_billing_date') if next_billing_date_str: next_billing_date = datetime.strptime(next_billing_date_str, '%Y-%m-%d').date() days_in_billing_cycle = (next_billing_date - billing_date).days else: # Default to 30 days if next_billing_date is not set days_in_billing_cycle = 30 if days_in_billing_cycle <= 0: return 0 # Calculate proportional cost already paid # (days_since_billing / days_in_billing_cycle) * monthly_cost already_paid = int((days_since_billing / days_in_billing_cycle) * current_monthly_cost) logger.info(f"Already paid calculation for server {server_id}:") logger.info(f" Days since billing: {days_since_billing}") logger.info(f" Days in billing cycle: {days_in_billing_cycle}") logger.info(f" Current monthly cost: {current_monthly_cost}") logger.info(f" Already paid: {already_paid}") return already_paid except Exception as e: logger.error(f"Error calculating already paid amount: {e}") return 0 def get_active_servers(): """Pobierz wszystkie aktywne serwery (w stanie Running lub Starting)""" try: with database_connection() as conn: cursor = conn.cursor() cursor.execute(''' SELECT s.*, u.username, u.email, u.credits FROM servers s JOIN users u ON s.owner_id = u.id WHERE s.status IN ('Running', 'Starting') ''') servers = [dict_from_row(row) for row in cursor.fetchall()] for server in servers: if server and 'ports' in server: server['ports'] = json.loads(server['ports']) return servers except Exception as e: logger.error(f"Error getting active servers: {e}") return [] def charge_monthly_fees(): """Ściągnij opłaty miesięczne za wszystkie aktywne serwery""" try: logger.info("🔍 Starting monthly server fee collection...") active_servers = get_active_servers() total_servers = len(active_servers) successful_charges = 0 failed_charges = 0 insufficient_credits = 0 logger.info(f"Found {total_servers} active servers") for server in active_servers: try: user_id = server['owner_id'] server_id = server['id'] server_name = server['name'] user_credits = server['credits'] # Oblicz miesięczny koszt monthly_cost = calculate_monthly_server_cost(server) logger.info(f"Processing server: {server_name} (ID: {server_id})") logger.info(f" User credits: {user_credits}") logger.info(f" Monthly cost: {monthly_cost} credits") # Always deduct credits, regardless of balance new_credits = user_credits - monthly_cost if update_user_credits(user_id, new_credits): # Add transaction record add_credit_transaction( user_id, -monthly_cost, 'monthly_fee', f'Monthly server fee: {server_name} ({server["type"]}) - {monthly_cost} credits' ) # Save fee record save_monthly_fee_record(user_id, server_id, monthly_cost) # Check if balance is negative if new_credits < 0: insufficient_credits += 1 logger.warning(f"⚠ Insufficient credits for user {server['username']}: {new_credits} < 0") # Stop server due to negative balance try: stop_server(server_id, user_id) logger.info(f"🛑 Stopped server {server_name} due to negative balance") # Send suspension email try: email_svc = get_email_service() if email_svc: email_svc.send_server_suspended_email( server['email'], server['username'], server_name, monthly_cost, new_credits ) except Exception as email_error: logger.error(f"Error sending suspension email: {email_error}") except Exception as stop_error: logger.error(f"Error stopping server: {stop_error}") else: # Balance is positive (or zero), server keeps running successful_charges += 1 logger.info(f"✅ Successfully charged {monthly_cost} credits for server {server_name}") # Send confirmation email try: email_svc = get_email_service() if email_svc: email_svc.send_monthly_fee_email( server['email'], server['username'], server_name, monthly_cost, new_credits ) except Exception as email_error: logger.error(f"Error sending fee email: {email_error}") else: failed_charges += 1 logger.error(f"❌ Failed to update credits for user {user_id}") except Exception as server_error: failed_charges += 1 logger.error(f"Error processing server {server.get('id', 'unknown')}: {server_error}") # Podsumowanie logger.info(f"💰 Monthly fee collection completed:") logger.info(f" • Total servers: {total_servers}") logger.info(f" • Successfully charged: {successful_charges}") logger.info(f" • Failed charges: {failed_charges}") logger.info(f" • Insufficient credits: {insufficient_credits}") return { 'success': True, 'total_servers': total_servers, 'successful_charges': successful_charges, 'failed_charges': failed_charges, 'insufficient_credits': insufficient_credits, 'timestamp': datetime.now().isoformat() } except Exception as e: logger.error(f"Error in monthly fee collection: {e}") return { 'success': False, 'error': str(e), 'timestamp': datetime.now().isoformat() } def save_monthly_fee_record(user_id: str, server_id: str, amount: int) -> bool: """Zapisz rekord opłaty miesięcznej w bazie danych""" try: with database_connection() as conn: cursor = conn.cursor() # Sprawdź czy tabela monthly_fees istnieje, jeśli nie - utwórz cursor.execute(''' CREATE TABLE IF NOT EXISTS monthly_fees ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, server_id TEXT NOT NULL, amount INTEGER NOT NULL, payment_date DATE NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, FOREIGN KEY (server_id) REFERENCES servers (id) ON DELETE CASCADE ) ''') # Utwórz indeksy cursor.execute('CREATE INDEX IF NOT EXISTS idx_monthly_fees_user_id ON monthly_fees(user_id)') cursor.execute('CREATE INDEX IF NOT EXISTS idx_monthly_fees_server_id ON monthly_fees(server_id)') cursor.execute('CREATE INDEX IF NOT EXISTS idx_monthly_fees_payment_date ON monthly_fees(payment_date)') # Wstaw rekord fee_id = f"fee-{str(uuid.uuid4())[:8]}" payment_date = datetime.now().date().isoformat() cursor.execute(''' INSERT INTO monthly_fees (id, user_id, server_id, amount, payment_date) VALUES (?, ?, ?, ?, ?) ''', (fee_id, user_id, server_id, amount, payment_date)) conn.commit() return True except Exception as e: logger.error(f"Error saving monthly fee record: {e}") return False def get_user_monthly_fees(user_id: str, limit: int = 10) -> List[Dict]: """Pobierz historię opłat miesięcznych użytkownika""" try: with database_connection() as conn: cursor = conn.cursor() cursor.execute(''' SELECT mf.*, s.name as server_name, s.type as server_type FROM monthly_fees mf JOIN servers s ON mf.server_id = s.id WHERE mf.user_id = ? ORDER BY mf.payment_date DESC LIMIT ? ''', (user_id, limit)) fees = [dict_from_row(row) for row in cursor.fetchall()] return fees except Exception as e: logger.error(f"Error getting user monthly fees: {e}") return [] def get_user_credit_history(user_id: str, limit: int = 10) -> List[Dict]: """Get credit transaction history for a user""" try: with database_connection() as conn: cursor = conn.cursor() cursor.execute(''' SELECT * FROM credit_transactions WHERE user_id = ? ORDER BY created_at DESC LIMIT ? ''', (user_id, limit)) transactions = [dict_from_row(row) for row in cursor.fetchall()] return transactions except Exception as e: logger.error(f"Error getting user credit history: {e}") return [] def start_monthly_fee_scheduler(): """Uruchom scheduler do automatycznego ściągania opłat""" def run_scheduler(): # Ustaw harmonogram - każdego 1-go dnia miesiąca o północy schedule.every().day.at("00:00").do(check_and_charge_fees) logger.info("💰 Monthly fee scheduler started") logger.info(" Will check for fees on the 1st day of each month at midnight") while True: try: schedule.run_pending() time.sleep(60) # Sprawdzaj co minutę except Exception as e: logger.error(f"Scheduler error: {e}") time.sleep(300) # Czekaj 5 minut w przypadku błędu thread = threading.Thread(target=run_scheduler, daemon=True) thread.start() def check_and_charge_fees(): """Sprawdź czy dzisiaj jest 1-szy dzień miesiąca i pobierz opłaty""" today = datetime.now() # Sprawdź czy dzisiaj jest 1-szy dzień miesiąca if today.day == 1: logger.info(f"📅 Today is the 1st day of month {today.month}, charging monthly fees...") result = charge_monthly_fees() logger.info(f"Monthly fee result: {result}") # Zapisz log opłat save_fee_collection_log(result) else: logger.debug(f"Not the 1st day of month (today: {today.day}), skipping fee collection") def save_fee_collection_log(result: Dict) -> bool: """Zapisz log kolekcji opłat""" try: with database_connection() as conn: cursor = conn.cursor() # Utwórz tabelę jeśli nie istnieje cursor.execute(''' CREATE TABLE IF NOT EXISTS fee_collection_logs ( id TEXT PRIMARY KEY, total_servers INTEGER NOT NULL, successful_charges INTEGER NOT NULL, failed_charges INTEGER NOT NULL, insufficient_credits INTEGER NOT NULL, collection_date DATE NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') # Wstaw rekord log_id = f"log-{str(uuid.uuid4())[:8]}" collection_date = datetime.now().date().isoformat() cursor.execute(''' INSERT INTO fee_collection_logs (id, total_servers, successful_charges, failed_charges, insufficient_credits, collection_date) VALUES (?, ?, ?, ?, ?, ?) ''', ( log_id, result.get('total_servers', 0), result.get('successful_charges', 0), result.get('failed_charges', 0), result.get('insufficient_credits', 0), collection_date )) conn.commit() return True except Exception as e: logger.error(f"Error saving fee collection log: {e}") return False