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,484 @@
|
||||
"""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
|
||||
Reference in new issue
Block a user