108 lines
3.6 KiB
Python
108 lines
3.6 KiB
Python
"""Exchange-rate retrieval and Stripe currency conversion."""
|
|
|
|
|
|
from datetime import datetime
|
|
import requests
|
|
from runtime_config import (
|
|
CACHE_DURATION,
|
|
EXCHANGE_RATE_CACHE,
|
|
EXCHANGE_RATE_LOCK,
|
|
logger,
|
|
)
|
|
|
|
__all__ = ['calculate_stripe_amount', 'fetch_exchange_rates', 'get_cached_exchange_rates']
|
|
|
|
|
|
|
|
def calculate_stripe_amount(credits: int, currency: str) -> int:
|
|
"""
|
|
Calculate amount in smallest currency unit (cents, pence, etc.)
|
|
Base rate: 100 credits = 1.00 USD
|
|
"""
|
|
# Get USD price
|
|
usd_price = credits / 100 # $1 per 100 credits
|
|
|
|
# Get exchange rate
|
|
rates = get_cached_exchange_rates()
|
|
rate = rates.get(currency, 1.0)
|
|
|
|
# Convert to target currency
|
|
currency_price = usd_price * rate
|
|
|
|
# Convert to smallest unit (cents)
|
|
# Most currencies use 2 decimal places, but some don't
|
|
if currency in ['JPY', 'KRW']: # Zero decimal currencies
|
|
return int(currency_price)
|
|
else:
|
|
return int(currency_price * 100)
|
|
|
|
def fetch_exchange_rates():
|
|
"""
|
|
Fetch exchange rates from the free currency API
|
|
Uses EUR as base currency
|
|
"""
|
|
try:
|
|
# Using the fawazahmed0 currency API - EUR as base
|
|
url = 'https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/eur.json'
|
|
|
|
logger.info("Fetching exchange rates from API...")
|
|
response = requests.get(url, timeout=10)
|
|
response.raise_for_status()
|
|
|
|
data = response.json()
|
|
|
|
if 'eur' in data:
|
|
rates_data = data['eur']
|
|
|
|
# Extract rates for currencies we support - all relative to EUR
|
|
new_rates = {
|
|
'EUR': 1.00, # Base currency
|
|
'USD': rates_data.get('usd', 1.5), # EUR->USD rate
|
|
'GBP': rates_data.get('gbp', 0.85), # EUR->GBP rate
|
|
'PLN': rates_data.get('pln', 4.32), # EUR->PLN rate
|
|
'PHP': rates_data.get('php', 60.50), # EUR->PHP rate
|
|
'CZK': rates_data.get('czk', 24.65) # EUR->CZK rate
|
|
}
|
|
|
|
with EXCHANGE_RATE_LOCK:
|
|
EXCHANGE_RATE_CACHE['rates'] = new_rates
|
|
EXCHANGE_RATE_CACHE['last_updated'] = datetime.now().isoformat()
|
|
|
|
logger.info(f"✅ Exchange rates updated successfully: {new_rates}")
|
|
return True
|
|
else:
|
|
logger.error("Invalid response format from currency API")
|
|
return False
|
|
|
|
except requests.exceptions.Timeout:
|
|
logger.error("❌ Timeout fetching exchange rates")
|
|
return False
|
|
except requests.exceptions.RequestException as e:
|
|
logger.error(f"❌ Error fetching exchange rates: {e}")
|
|
return False
|
|
except Exception as e:
|
|
logger.error(f"❌ Unexpected error fetching exchange rates: {e}")
|
|
return False
|
|
|
|
def get_cached_exchange_rates():
|
|
"""Get cached exchange rates, fetch new ones if cache is stale"""
|
|
with EXCHANGE_RATE_LOCK:
|
|
last_updated = EXCHANGE_RATE_CACHE.get('last_updated')
|
|
|
|
# If never updated or cache is stale, fetch new rates
|
|
if last_updated is None:
|
|
logger.info("Exchange rates cache is empty, fetching...")
|
|
# Release lock before fetching to avoid deadlock
|
|
elif (datetime.now() - datetime.fromisoformat(last_updated)).total_seconds() > CACHE_DURATION:
|
|
logger.info("Exchange rates cache is stale, fetching...")
|
|
else:
|
|
# Cache is fresh, return it
|
|
return EXCHANGE_RATE_CACHE['rates']
|
|
|
|
# Fetch new rates (outside lock to avoid blocking)
|
|
fetch_exchange_rates()
|
|
|
|
# Return rates (either newly fetched or fallback)
|
|
with EXCHANGE_RATE_LOCK:
|
|
return EXCHANGE_RATE_CACHE['rates']
|