Refactor backend and add registry build workflow
Build and publish Docker image / build-and-publish (push) Successful in 2m45s

This commit is contained in:
wfalkowski committed 2026-10-20 16:44:49 +00:00
commit c177d7981d
43 files changed
+18124

No files matched your search

+383
View File
@@ -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