384 lines
15 KiB
Python
384 lines
15 KiB
Python
"""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
|