Files
anchorhost-backend/routes/billing.py
T
wfalkowski c177d7981d
Build and publish Docker image / build-and-publish (push) Successful in 2m45s
Refactor backend and add registry build workflow
2026-10-20 16:44:49 +00:00

485 lines
17 KiB
Python

"""Billing API routes for AnchorHost."""
from currency_service import calculate_stripe_amount, get_cached_exchange_rates
from database_service import database_connection, dict_from_row
from email_service import get_email_service
from flask import Blueprint, jsonify, request
from runtime_config import (
CACHE_DURATION,
EXCHANGE_RATE_CACHE,
STRIPE_CURRENCY_MAP,
STRIPE_SECRET_KEY,
STRIPE_WEBHOOK_SECRET,
logger,
)
from server_billing import get_user_monthly_fees
from server_capacity import calculate_monthly_server_cost
from server_records import get_user_servers
import stripe
from user_service import add_credit_transaction, authenticate_user, get_bearer_token, get_user_by_id, update_user_credits
__all__ = ['create_checkout_session', 'estimate_monthly_cost', 'get_billing_history', 'get_exchange_rates_api', 'get_session_status', 'payment_cancelled', 'payment_success', 'stripe_webhook']
stripe.api_key = STRIPE_SECRET_KEY
bp = Blueprint("billing", __name__)
@bp.route('/api/user/billing/history', methods=['GET'])
def get_billing_history():
"""Get user's billing history"""
try:
token = get_bearer_token()
user = authenticate_user(token)
if not user:
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
limit = request.args.get('limit', 10, type=int)
# Pobierz opłaty miesięczne
monthly_fees = get_user_monthly_fees(user['id'], limit)
# Pobierz informacje o serwerach i datach rozliczeniowych
servers = get_user_servers(user['id'])
server_billing_info = []
for server in servers:
with database_connection() as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT * FROM server_billing WHERE server_id = ?
''', (server['id'],))
billing = dict_from_row(cursor.fetchone())
if billing:
server['billing_info'] = billing
server_billing_info.append(server)
return jsonify({
'success': True,
'monthly_fees': monthly_fees,
'active_servers': server_billing_info,
'total_active_servers': len([s for s in servers if s['status'] in ['Running', 'Starting']])
}), 200
except Exception as e:
logger.error(f"Error getting billing history: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/api/user/billing/estimate', methods=['POST'])
def estimate_monthly_cost():
"""Estimate monthly cost for a server configuration"""
try:
token = get_bearer_token()
user = authenticate_user(token)
if not user:
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
data = request.get_json()
if not data:
return jsonify({'success': False, 'error': 'No configuration provided'}), 400
# Utwórz mock serwer do kalkulacji
mock_server = {
'cpu': data.get('cpu', '1 vCPU'),
'ram': data.get('ram', '1GB'),
'storage': data.get('storage', '10GB'),
'type': data.get('serverType', 'vps'),
'name': 'Estimation'
}
monthly_cost = calculate_monthly_server_cost(mock_server)
return jsonify({
'success': True,
'monthly_cost_credits': monthly_cost,
'monthly_cost_euro': monthly_cost / 100, # 100 kredytów = 1 euro
'estimated_daily_cost': monthly_cost / 30, # Dzienny koszt
'currency': 'EUR',
'pricing_model': {
'cpu_per_vcpu': '0.25 EUR',
'ram_per_gb': '0.25 EUR',
'storage_per_gb': '0.05 EUR',
'server_type_multipliers': {
'vps': '1.0x',
'game': '1.5x',
'website': '1.2x',
'database': '1.3x'
}
}
}), 200
except Exception as e:
logger.error(f"Error estimating monthly cost: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@bp.route('/api/stripe/create-checkout-session', methods=['POST'])
def create_checkout_session():
"""Create a Stripe checkout session"""
try:
token = get_bearer_token()
user = authenticate_user(token)
if not user:
return jsonify({'error': 'Unauthorized'}), 401
data = request.get_json()
credits = data.get('credits', 1000)
currency = data.get('currency', 'USD').upper()
if credits < 100:
return jsonify({'error': 'Minimum purchase is 100 credits'}), 400
if currency not in STRIPE_CURRENCY_MAP:
return jsonify({'error': f'Currency {currency} not supported'}), 400
# Calculate amount in smallest currency unit
amount = calculate_stripe_amount(credits, currency)
stripe_currency = STRIPE_CURRENCY_MAP[currency]
# Get currency symbol for display
currency_data = next((c for c in [
{'code': 'USD', 'symbol': '$'},
{'code': 'EUR', 'symbol': '€'},
{'code': 'GBP', 'symbol': '£'},
{'code': 'PLN', 'symbol': 'zł'},
{'code': 'PHP', 'symbol': '₱'},
{'code': 'CZK', 'symbol': 'Kč'}
] if c['code'] == currency), {'code': currency, 'symbol': currency})
# Create Stripe checkout session
session = stripe.checkout.Session.create(
payment_method_types=[
'card',
'paypal',
'blik',
'revolut_pay',
'ideal',
'bancontact',
'eps',
'twint',
'link',
'multibanco',
'klarna'
],
line_items=[{
'price_data': {
'currency': stripe_currency,
'product_data': {
'name': f'{credits} AnchorHost Credits',
'description': f'Purchase {credits} credits for your AnchorHost account',
'images': ['https://your-logo-url.com/logo.png'], # Optional: Add your logo
},
'unit_amount': amount,
},
'quantity': 1,
}],
mode='payment',
success_url=request.host_url + f'payment-success?session_id={{CHECKOUT_SESSION_ID}}',
cancel_url=request.host_url + 'payment-cancelled',
metadata={
'user_id': user['id'],
'username': user['username'],
'credits': str(credits),
'currency': currency
},
customer_email=user['email'],
)
logger.info(f"Created Stripe checkout session for user {user['username']}: {credits} credits, {currency}")
return jsonify({
'success': True,
'sessionId': session.id,
'url': session.url
}), 200
except stripe.error.StripeError as e:
logger.error(f"Stripe error: {e}")
return jsonify({'error': f'Payment service error: {str(e)}'}), 500
except Exception as e:
logger.error(f"Error creating checkout session: {e}")
return jsonify({'error': str(e)}), 500
@bp.route('/api/stripe/webhook', methods=['POST'])
def stripe_webhook():
"""Handle Stripe webhook events"""
payload = request.get_data()
sig_header = request.headers.get('Stripe-Signature')
try:
event = stripe.Webhook.construct_event(
payload, sig_header, STRIPE_WEBHOOK_SECRET
)
except ValueError as e:
logger.error(f"Invalid payload: {e}")
return jsonify({'error': 'Invalid payload'}), 400
except stripe.error.SignatureVerificationError as e:
logger.error(f"Invalid signature: {e}")
return jsonify({'error': 'Invalid signature'}), 400
# Handle the event
if event['type'] == 'checkout.session.completed':
session = event['data']['object']
# Get metadata
user_id = session['metadata'].get('user_id')
username = session['metadata'].get('username')
credits = int(session['metadata'].get('credits', 0))
currency = session['metadata'].get('currency', 'USD')
logger.info(f"Payment completed: {username} purchased {credits} credits for {session['amount_total']/100} {currency}")
# Add credits to user account
try:
user = get_user_by_id(user_id)
if user:
new_credits = user.get('credits', 0) + credits
if update_user_credits(user_id, new_credits):
add_credit_transaction(
user_id,
credits,
'purchase',
f'Stripe payment: {credits} credits purchased for {session["amount_total"]/100} {currency}'
)
logger.info(f"✅ Credits added to {username}: +{credits} (new balance: {new_credits})")
# Send confirmation email
email_svc = get_email_service()
if email_svc:
try:
email_svc.send_payment_confirmation(
user['email'],
username,
credits,
session['amount_total'] / 100,
currency
)
except Exception as e:
logger.error(f"Failed to send confirmation email: {e}")
else:
logger.error(f"Failed to update credits for {username}")
else:
logger.error(f"User not found: {user_id}")
except Exception as e:
logger.error(f"Error processing payment for {username}: {e}")
elif event['type'] == 'checkout.session.async_payment_succeeded':
# Handle async payment success (e.g., bank transfers)
session = event['data']['object']
logger.info(f"Async payment succeeded: {session['id']}")
elif event['type'] == 'checkout.session.async_payment_failed':
# Handle async payment failure
session = event['data']['object']
logger.warning(f"Async payment failed: {session['id']}")
return jsonify({'success': True}), 200
@bp.route('/api/stripe/session-status/<session_id>', methods=['GET'])
def get_session_status(session_id):
"""Get the status of a Stripe checkout session"""
try:
token = get_bearer_token()
user = authenticate_user(token)
if not user:
return jsonify({'error': 'Unauthorized'}), 401
session = stripe.checkout.Session.retrieve(session_id)
return jsonify({
'success': True,
'status': session.status,
'payment_status': session.payment_status,
'amount_total': session.amount_total,
'currency': session.currency,
'customer_email': session.customer_email
}), 200
except stripe.error.StripeError as e:
logger.error(f"Stripe error: {e}")
return jsonify({'error': str(e)}), 500
except Exception as e:
logger.error(f"Error getting session status: {e}")
return jsonify({'error': str(e)}), 500
@bp.route('/payment-success')
def payment_success():
"""Payment success page"""
session_id = request.args.get('session_id')
return f'''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Payment Successful</title>
<style>
body {{
font-family: Arial, sans-serif;
background: #0f0f0f;
color: #cbcbcd;
margin: 0;
padding: 40px;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}}
.container {{
max-width: 600px;
background: #1a1a1a;
border-radius: 12px;
padding: 40px;
text-align: center;
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
}}
h1 {{ color: #6b7d55; margin: 0 0 20px 0; font-size: 32px; }}
.icon {{ font-size: 64px; margin-bottom: 20px; }}
p {{ color: #8a8a8a; line-height: 1.6; margin-bottom: 30px; }}
.btn {{
display: inline-block;
padding: 16px 40px;
background: linear-gradient(135deg, #5a7ca7 0%, #4a6385 100%);
color: #fff;
text-decoration: none;
border-radius: 8px;
font-weight: bold;
font-size: 16px;
transition: all 0.3s;
}}
.btn:hover {{
background: linear-gradient(135deg, #4a6385 0%, #3a5375 100%);
transform: translateY(-2px);
}}
.session-id {{
background: #0f0f0f;
padding: 10px;
border-radius: 6px;
font-family: monospace;
font-size: 12px;
color: #5a7ca7;
margin-top: 20px;
word-break: break-all;
}}
</style>
<script>
// Auto-close after 5 seconds and notify parent window
setTimeout(() => {{
if (window.opener) {{
window.opener.postMessage({{
type: 'payment_success',
sessionId: '{session_id}'
}}, '*');
}}
window.close();
}}, 5000);
</script>
</head>
<body>
<div class="container">
<div class="icon">✅</div>
<h1>Payment Successful!</h1>
<p>Your credits have been added to your account. Thank you for your purchase!</p>
<p style="font-size: 14px;">This window will close automatically in 5 seconds...</p>
<a href="/" class="btn" onclick="window.close(); return false;">Return to Dashboard</a>
<div class="session-id">Session ID: {session_id}</div>
</div>
</body>
</html>
'''
@bp.route('/payment-cancelled')
def payment_cancelled():
"""Payment cancelled page"""
return '''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Payment Cancelled</title>
<style>
body {{
font-family: Arial, sans-serif;
background: #0f0f0f;
color: #cbcbcd;
margin: 0;
padding: 40px;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}}
.container {{
max-width: 600px;
background: #1a1a1a;
border-radius: 12px;
padding: 40px;
text-align: center;
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
}}
h1 {{ color: #9c6d2f; margin: 0 0 20px 0; font-size: 32px; }}
.icon {{ font-size: 64px; margin-bottom: 20px; }}
p {{ color: #8a8a8a; line-height: 1.6; margin-bottom: 30px; }}
.btn {{
display: inline-block;
padding: 16px 40px;
background: linear-gradient(135deg, #5a7ca7 0%, #4a6385 100%);
color: #fff;
text-decoration: none;
border-radius: 8px;
font-weight: bold;
font-size: 16px;
transition: all 0.3s;
}}
.btn:hover {{
background: linear-gradient(135deg, #4a6385 0%, #3a5375 100%);
transform: translateY(-2px);
}}
</style>
<script>
// Auto-close after 3 seconds
setTimeout(() => {
if (window.opener) {
window.opener.postMessage({type: 'payment_cancelled'}, '*');
}
window.close();
}, 3000);
</script>
</head>
<body>
<div class="container">
<div class="icon">❌</div>
<h1>Payment Cancelled</h1>
<p>Your payment was cancelled. No charges were made to your account.</p>
<p style="font-size: 14px;">This window will close automatically in 3 seconds...</p>
<a href="/" class="btn" onclick="window.close(); return false;">Return to Dashboard</a>
</div>
</body>
</html>
'''
@bp.route('/api/exchange-rates', methods=['GET'])
def get_exchange_rates_api():
"""Get current exchange rates"""
try:
rates = get_cached_exchange_rates()
return jsonify({
'success': True,
'rates': rates,
'base_currency': 'USD',
'last_updated': EXCHANGE_RATE_CACHE.get('last_updated'),
'cache_duration_seconds': CACHE_DURATION
}), 200
except Exception as e:
logger.error(f"Error getting exchange rates: {e}")
return jsonify({
'success': False,
'error': str(e)
}), 500