191 lines
6.6 KiB
Python
191 lines
6.6 KiB
Python
"""
|
|
DragonBank - Service de Calcul des Intérêts
|
|
============================================
|
|
Service indépendant qui calcule et applique les intérêts
|
|
sur les comptes épargne (Livret A, Assurance Vie).
|
|
|
|
Bonus: +3% journalier sur les comptes épargne.
|
|
"""
|
|
|
|
import os
|
|
import time
|
|
import decimal
|
|
from datetime import datetime, timezone
|
|
|
|
import psycopg2
|
|
import psycopg2.extras
|
|
import schedule
|
|
|
|
# ============================================
|
|
# CONFIGURATION
|
|
# ============================================
|
|
DATABASE_URL = os.environ.get(
|
|
'DATABASE_URL',
|
|
'postgresql://dragonadmin:dragonpass@db:5432/dragonbank'
|
|
)
|
|
INTEREST_RATE_LIVRET_A = float(os.environ.get('INTEREST_RATE_LIVRET_A', 0.03))
|
|
INTEREST_RATE_ASSURANCE_VIE = float(os.environ.get('INTEREST_RATE_ASSURANCE_VIE', 0.02))
|
|
INTERVAL_SECONDS = int(os.environ.get('INTERVAL_SECONDS', 86400)) # 24h par défaut
|
|
|
|
|
|
def get_db_connection():
|
|
"""Crée une connexion à la base de données."""
|
|
conn = psycopg2.connect(
|
|
DATABASE_URL,
|
|
cursor_factory=psycopg2.extras.RealDictCursor
|
|
)
|
|
return conn
|
|
|
|
|
|
def calculate_interests():
|
|
"""
|
|
Calcule et applique les intérêts sur tous les comptes épargne actifs.
|
|
|
|
- Livret A: INTEREST_RATE_LIVRET_A (par défaut 3%)
|
|
- Assurance Vie: INTEREST_RATE_ASSURANCE_VIE (par défaut 2%)
|
|
"""
|
|
print(f"\n{'='*60}")
|
|
print(f"🐉 CALCUL DES INTÉRÊTS - {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}")
|
|
print(f"{'='*60}")
|
|
|
|
conn = get_db_connection()
|
|
try:
|
|
cur = conn.cursor()
|
|
|
|
# Récupérer tous les comptes épargne actifs
|
|
cur.execute('''
|
|
SELECT id, user_id, account_number, account_type, balance, interest_rate
|
|
FROM accounts
|
|
WHERE status = 'active'
|
|
AND account_type IN ('livret_a', 'assurance_vie')
|
|
AND balance > 0
|
|
''')
|
|
|
|
accounts = cur.fetchall()
|
|
total_interest_paid = decimal.Decimal('0.00')
|
|
accounts_processed = 0
|
|
|
|
for account in accounts:
|
|
account_id = str(account['id'])
|
|
balance = decimal.Decimal(str(account['balance']))
|
|
account_type = account['account_type']
|
|
|
|
# Déterminer le taux d'intérêt
|
|
if account_type == 'livret_a':
|
|
rate = decimal.Decimal(str(INTEREST_RATE_LIVRET_A))
|
|
elif account_type == 'assurance_vie':
|
|
rate = decimal.Decimal(str(INTEREST_RATE_ASSURANCE_VIE))
|
|
else:
|
|
continue
|
|
|
|
# Calculer les intérêts
|
|
interest_amount = (balance * rate).quantize(decimal.Decimal('0.01'))
|
|
|
|
if interest_amount <= 0:
|
|
continue
|
|
|
|
new_balance = balance + interest_amount
|
|
|
|
# Appliquer les intérêts
|
|
cur.execute(
|
|
'UPDATE accounts SET balance = %s, updated_at = NOW() WHERE id = %s',
|
|
(float(new_balance), account_id)
|
|
)
|
|
|
|
# Enregistrer dans l'historique des intérêts
|
|
cur.execute('''
|
|
INSERT INTO interest_history
|
|
(account_id, amount, rate, balance_before, balance_after)
|
|
VALUES (%s, %s, %s, %s, %s)
|
|
''', (
|
|
account_id,
|
|
float(interest_amount),
|
|
float(rate),
|
|
float(balance),
|
|
float(new_balance)
|
|
))
|
|
|
|
# Créer une transaction pour traçabilité
|
|
cur.execute('''
|
|
INSERT INTO transactions
|
|
(to_account_id, transaction_type, amount, description, status, executed_at)
|
|
VALUES (%s, 'interets', %s, %s, 'completed', NOW())
|
|
''', (
|
|
account_id,
|
|
float(interest_amount),
|
|
f"Intérêts {account_type.replace('_', ' ').title()} ({float(rate)*100:.1f}%)"
|
|
))
|
|
|
|
total_interest_paid += interest_amount
|
|
accounts_processed += 1
|
|
|
|
print(f" ✅ {account['account_number']} ({account_type}): "
|
|
f"{float(balance):.2f}€ → {float(new_balance):.2f}€ "
|
|
f"(+{float(interest_amount):.2f}€ à {float(rate)*100:.1f}%)")
|
|
|
|
conn.commit()
|
|
|
|
print(f"\n📊 Résumé:")
|
|
print(f" Comptes traités: {accounts_processed}")
|
|
print(f" Total intérêts versés: {float(total_interest_paid):.2f}€")
|
|
print(f"{'='*60}\n")
|
|
|
|
except Exception as e:
|
|
conn.rollback()
|
|
print(f" ❌ ERREUR lors du calcul des intérêts: {e}")
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def main():
|
|
"""Point d'entrée principal du service d'intérêts."""
|
|
print("🐉" + "="*58)
|
|
print(" DragonBank - Service de Calcul des Intérêts")
|
|
print("="*60)
|
|
print(f" Taux Livret A: {INTEREST_RATE_LIVRET_A*100:.1f}% / cycle")
|
|
print(f" Taux Assurance Vie: {INTEREST_RATE_ASSURANCE_VIE*100:.1f}% / cycle")
|
|
print(f" Intervalle: {INTERVAL_SECONDS} secondes")
|
|
print(f" Database: {DATABASE_URL.split('@')[1] if '@' in DATABASE_URL else 'configured'}")
|
|
print("="*60)
|
|
|
|
# Attendre que la DB soit prête
|
|
print("\n⏳ Attente de la connexion à la base de données...")
|
|
max_retries = 30
|
|
for i in range(max_retries):
|
|
try:
|
|
conn = get_db_connection()
|
|
conn.close()
|
|
print("✅ Connexion à la base de données établie !")
|
|
break
|
|
except Exception as e:
|
|
if i < max_retries - 1:
|
|
print(f" Tentative {i+1}/{max_retries} échouée: {e}")
|
|
time.sleep(2)
|
|
else:
|
|
print(f"❌ Impossible de se connecter à la DB après {max_retries} tentatives")
|
|
return
|
|
|
|
# Premier calcul immédiat
|
|
print("\n🚀 Premier calcul des intérêts...")
|
|
calculate_interests()
|
|
|
|
# Planifier les calculs suivants
|
|
# Pour la démo, on peut réduire l'intervalle
|
|
if INTERVAL_SECONDS < 3600:
|
|
# Si intervalle court (démo), utiliser un simple sleep
|
|
print(f"\n⏰ Mode démo: calcul toutes les {INTERVAL_SECONDS} secondes")
|
|
while True:
|
|
time.sleep(INTERVAL_SECONDS)
|
|
calculate_interests()
|
|
else:
|
|
# En production, calcul journalier
|
|
schedule.every(INTERVAL_SECONDS).seconds.do(calculate_interests)
|
|
print(f"\n⏰ Prochain calcul dans {INTERVAL_SECONDS} secondes")
|
|
|
|
while True:
|
|
schedule.run_pending()
|
|
time.sleep(60)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main() |