maj
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
LABEL maintainer="DragonBank Team"
|
||||
LABEL description="DragonBank Interest Calculator Service"
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc \
|
||||
libpq-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir --upgrade pip && \
|
||||
pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY app.py .
|
||||
|
||||
RUN adduser --disabled-password --gecos '' appuser && \
|
||||
chown -R appuser:appuser /app
|
||||
USER appuser
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
|
||||
CMD python -c "import psycopg2; psycopg2.connect('${DATABASE_URL}')" || exit 1
|
||||
|
||||
CMD ["python", "app.py"]
|
||||
@@ -0,0 +1,191 @@
|
||||
"""
|
||||
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()
|
||||
@@ -0,0 +1,2 @@
|
||||
psycopg2-binary==2.9.9
|
||||
schedule==1.2.1
|
||||
Reference in New Issue
Block a user