Refactor backend and add registry build workflow
Build and publish Docker image / build-and-publish (push) Successful in 2m45s
Build and publish Docker image / build-and-publish (push) Successful in 2m45s
This commit is contained in:
commit
c177d7981d
43 files changed
+18124
No files matched your search
@@ -0,0 +1,383 @@
|
||||
"""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
|
||||
Reference in new issue
Block a user