""" DragonBank - Backend API ======================== API REST pour la gestion bancaire complète. """ import os import uuid import decimal from datetime import datetime, timedelta, timezone from functools import wraps import jwt import bcrypt import psycopg2 import psycopg2.extras from flask import Flask, request, jsonify from flask_cors import CORS # ============================================ # CONFIGURATION # ============================================ app = Flask(__name__) CORS(app) app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-secret-key') DATABASE_URL = os.environ.get('DATABASE_URL', 'postgresql://dragonadmin:dragonpass@db:5432/dragonbank') # ============================================ # UTILITAIRES BASE DE DONNÉES # ============================================ def get_db_connection(): """Crée une connexion à la base de données PostgreSQL.""" conn = psycopg2.connect( DATABASE_URL, cursor_factory=psycopg2.extras.RealDictCursor ) conn.autocommit = False return conn def decimal_to_float(obj): """Convertit les objets Decimal en float pour la sérialisation JSON.""" if isinstance(obj, decimal.Decimal): return float(obj) if isinstance(obj, datetime): return obj.isoformat() if isinstance(obj, uuid.UUID): return str(obj) return obj def serialize_row(row): """Sérialise une ligne de résultat de la DB.""" if row is None: return None return {k: decimal_to_float(v) for k, v in dict(row).items()} def serialize_rows(rows): """Sérialise plusieurs lignes.""" return [serialize_row(row) for row in rows] # ============================================ # AUTHENTIFICATION JWT # ============================================ def generate_token(user_id, email): """Génère un token JWT.""" payload = { 'user_id': str(user_id), 'email': email, 'exp': datetime.now(timezone.utc) + timedelta(hours=24), 'iat': datetime.now(timezone.utc) } return jwt.encode(payload, app.config['SECRET_KEY'], algorithm='HS256') def token_required(f): """Décorateur pour protéger les routes avec JWT.""" @wraps(f) def decorated(*args, **kwargs): token = None if 'Authorization' in request.headers: auth_header = request.headers['Authorization'] if auth_header.startswith('Bearer '): token = auth_header.split(' ')[1] if not token: return jsonify({'error': 'Token manquant'}), 401 try: data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256']) current_user_id = data['user_id'] except jwt.ExpiredSignatureError: return jsonify({'error': 'Token expiré'}), 401 except jwt.InvalidTokenError: return jsonify({'error': 'Token invalide'}), 401 return f(current_user_id, *args, **kwargs) return decorated # ============================================ # ROUTES - SANTÉ # ============================================ @app.route('/api/health', methods=['GET']) def health_check(): """Vérification de santé de l'API.""" try: conn = get_db_connection() cur = conn.cursor() cur.execute('SELECT 1') cur.close() conn.close() return jsonify({ 'status': 'healthy', 'service': 'DragonBank Backend API', 'database': 'connected', 'timestamp': datetime.now(timezone.utc).isoformat() }), 200 except Exception as e: return jsonify({ 'status': 'unhealthy', 'error': str(e) }), 500 # ============================================ # ROUTES - AUTHENTIFICATION # ============================================ @app.route('/api/auth/register', methods=['POST']) def register(): """Inscription d'un nouvel utilisateur.""" data = request.get_json() required_fields = ['email', 'password', 'first_name', 'last_name'] for field in required_fields: if field not in data or not data[field]: return jsonify({'error': f'Le champ {field} est requis'}), 400 # Validation email if '@' not in data['email'] or '.' not in data['email']: return jsonify({'error': 'Email invalide'}), 400 # Validation mot de passe (min 6 caractères) if len(data['password']) < 6: return jsonify({'error': 'Le mot de passe doit contenir au moins 6 caractères'}), 400 # Hash du mot de passe password_hash = bcrypt.hashpw( data['password'].encode('utf-8'), bcrypt.gensalt() ).decode('utf-8') conn = get_db_connection() try: cur = conn.cursor() # Vérifier si l'email existe déjà cur.execute('SELECT id FROM users WHERE email = %s', (data['email'],)) if cur.fetchone(): return jsonify({'error': 'Cet email est déjà utilisé'}), 409 # Créer l'utilisateur user_id = str(uuid.uuid4()) cur.execute(''' INSERT INTO users (id, email, password_hash, first_name, last_name, phone, address, date_of_birth) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) RETURNING id, email, first_name, last_name, created_at ''', ( user_id, data['email'], password_hash, data['first_name'], data['last_name'], data.get('phone', ''), data.get('address', ''), data.get('date_of_birth') )) user = cur.fetchone() # Créer automatiquement un compte courant account_number = f"DRG{str(uuid.uuid4().int)[:13].zfill(13)}" cur.execute(''' INSERT INTO accounts (user_id, account_number, account_type, balance) VALUES (%s, %s, 'courant', 100.00) ''', (user_id, account_number)) conn.commit() # Générer un token token = generate_token(user_id, data['email']) # Log d'audit cur.execute(''' INSERT INTO audit_logs (user_id, action, details, ip_address) VALUES (%s, %s, %s, %s) ''', (user_id, 'REGISTER', '{"action": "user_registered"}', request.remote_addr)) conn.commit() return jsonify({ 'message': 'Inscription réussie', 'user': serialize_row(user), 'token': token, 'account_number': account_number }), 201 except Exception as e: conn.rollback() return jsonify({'error': f'Erreur lors de l\'inscription: {str(e)}'}), 500 finally: conn.close() @app.route('/api/auth/login', methods=['POST']) def login(): """Connexion d'un utilisateur.""" data = request.get_json() if not data or 'email' not in data or 'password' not in data: return jsonify({'error': 'Email et mot de passe requis'}), 400 conn = get_db_connection() try: cur = conn.cursor() cur.execute(''' SELECT id, email, password_hash, first_name, last_name, is_active FROM users WHERE email = %s ''', (data['email'],)) user = cur.fetchone() if not user: return jsonify({'error': 'Email ou mot de passe incorrect'}), 401 if not user['is_active']: return jsonify({'error': 'Compte désactivé'}), 403 # Vérifier le mot de passe if not bcrypt.checkpw( data['password'].encode('utf-8'), user['password_hash'].encode('utf-8') ): return jsonify({'error': 'Email ou mot de passe incorrect'}), 401 # Mettre à jour le dernier login cur.execute( 'UPDATE users SET last_login = NOW() WHERE id = %s', (str(user['id']),) ) # Log d'audit cur.execute(''' INSERT INTO audit_logs (user_id, action, details, ip_address) VALUES (%s, %s, %s, %s) ''', (str(user['id']), 'LOGIN', '{"action": "user_login"}', request.remote_addr)) conn.commit() token = generate_token(user['id'], user['email']) return jsonify({ 'message': 'Connexion réussie', 'token': token, 'user': { 'id': str(user['id']), 'email': user['email'], 'first_name': user['first_name'], 'last_name': user['last_name'] } }), 200 except Exception as e: conn.rollback() return jsonify({'error': f'Erreur de connexion: {str(e)}'}), 500 finally: conn.close() # ============================================ # ROUTES - PROFIL UTILISATEUR # ============================================ @app.route('/api/user/profile', methods=['GET']) @token_required def get_profile(current_user_id): """Récupère le profil de l'utilisateur connecté.""" conn = get_db_connection() try: cur = conn.cursor() cur.execute(''' SELECT id, email, first_name, last_name, phone, address, date_of_birth, created_at, last_login FROM users WHERE id = %s ''', (current_user_id,)) user = cur.fetchone() if not user: return jsonify({'error': 'Utilisateur non trouvé'}), 404 return jsonify({'user': serialize_row(user)}), 200 finally: conn.close() # ============================================ # ROUTES - COMPTES BANCAIRES # ============================================ @app.route('/api/accounts', methods=['GET']) @token_required def get_accounts(current_user_id): """Récupère tous les comptes de l'utilisateur.""" conn = get_db_connection() try: cur = conn.cursor() cur.execute(''' SELECT id, account_number, account_type, balance, currency, status, interest_rate, created_at FROM accounts WHERE user_id = %s AND status = 'active' ORDER BY created_at ASC ''', (current_user_id,)) accounts = cur.fetchall() return jsonify({'accounts': serialize_rows(accounts)}), 200 finally: conn.close() @app.route('/api/accounts', methods=['POST']) @token_required def open_account(current_user_id): """Ouverture d'un nouveau compte bancaire.""" data = request.get_json() account_type = data.get('account_type', 'courant') valid_types = ['courant', 'livret_a', 'assurance_vie'] if account_type not in valid_types: return jsonify({'error': f'Type de compte invalide. Types valides: {valid_types}'}), 400 # Taux d'intérêt selon le type interest_rates = { 'courant': 0.0000, 'livret_a': 0.0300, 'assurance_vie': 0.0200 } conn = get_db_connection() try: cur = conn.cursor() # Vérifier qu'il n'a pas déjà ce type de compte (sauf courant) if account_type != 'courant': cur.execute(''' SELECT id FROM accounts WHERE user_id = %s AND account_type = %s AND status = 'active' ''', (current_user_id, account_type)) if cur.fetchone(): return jsonify({'error': f'Vous avez déjà un compte {account_type}'}), 409 account_number = f"DRG{str(uuid.uuid4().int)[:13].zfill(13)}" initial_balance = float(data.get('initial_deposit', 0)) cur.execute(''' INSERT INTO accounts (user_id, account_number, account_type, balance, interest_rate) VALUES (%s, %s, %s, %s, %s) RETURNING id, account_number, account_type, balance, interest_rate, created_at ''', ( current_user_id, account_number, account_type, initial_balance, interest_rates[account_type] )) account = cur.fetchone() # Si dépôt initial, créer une transaction if initial_balance > 0: cur.execute(''' INSERT INTO transactions (to_account_id, transaction_type, amount, description, status, executed_at) VALUES (%s, 'depot', %s, 'Dépôt initial à l''ouverture du compte', 'completed', NOW()) ''', (str(account['id']), initial_balance)) # Log d'audit cur.execute(''' INSERT INTO audit_logs (user_id, action, details, ip_address) VALUES (%s, %s, %s, %s) ''', ( current_user_id, 'OPEN_ACCOUNT', f'{{"account_type": "{account_type}", "account_number": "{account_number}"}}', request.remote_addr )) conn.commit() return jsonify({ 'message': f'Compte {account_type} ouvert avec succès', 'account': serialize_row(account) }), 201 except Exception as e: conn.rollback() return jsonify({'error': f'Erreur lors de l\'ouverture du compte: {str(e)}'}), 500 finally: conn.close() @app.route('/api/accounts/', methods=['GET']) @token_required def get_account_detail(current_user_id, account_id): """Détails d'un compte spécifique.""" conn = get_db_connection() try: cur = conn.cursor() cur.execute(''' SELECT id, account_number, account_type, balance, currency, status, interest_rate, created_at, updated_at FROM accounts WHERE id = %s AND user_id = %s ''', (account_id, current_user_id)) account = cur.fetchone() if not account: return jsonify({'error': 'Compte non trouvé'}), 404 return jsonify({'account': serialize_row(account)}), 200 finally: conn.close() # ============================================ # ROUTES - BÉNÉFICIAIRES # ============================================ @app.route('/api/beneficiaries', methods=['GET']) @token_required def get_beneficiaries(current_user_id): """Liste des bénéficiaires de l'utilisateur.""" conn = get_db_connection() try: cur = conn.cursor() cur.execute(''' SELECT id, beneficiary_name, bank_name, account_number, iban, bic, status, created_at FROM beneficiaries WHERE user_id = %s ORDER BY created_at DESC ''', (current_user_id,)) beneficiaries = cur.fetchall() return jsonify({'beneficiaries': serialize_rows(beneficiaries)}), 200 finally: conn.close() @app.route('/api/beneficiaries', methods=['POST']) @token_required def add_beneficiary(current_user_id): """Ajout d'un nouveau bénéficiaire.""" data = request.get_json() required_fields = ['beneficiary_name', 'account_number'] for field in required_fields: if field not in data or not data[field]: return jsonify({'error': f'Le champ {field} est requis'}), 400 conn = get_db_connection() try: cur = conn.cursor() # Vérifier que le bénéficiaire n'existe pas déjà cur.execute(''' SELECT id FROM beneficiaries WHERE user_id = %s AND account_number = %s ''', (current_user_id, data['account_number'])) if cur.fetchone(): return jsonify({'error': 'Ce bénéficiaire existe déjà'}), 409 # Vérifier que ce n'est pas son propre compte cur.execute(''' SELECT id FROM accounts WHERE user_id = %s AND account_number = %s ''', (current_user_id, data['account_number'])) if cur.fetchone(): return jsonify({'error': 'Vous ne pouvez pas vous ajouter vous-même comme bénéficiaire'}), 400 cur.execute(''' INSERT INTO beneficiaries (user_id, beneficiary_name, bank_name, account_number, iban, bic) VALUES (%s, %s, %s, %s, %s, %s) RETURNING id, beneficiary_name, bank_name, account_number, iban, bic, status, created_at ''', ( current_user_id, data['beneficiary_name'], data.get('bank_name', 'DragonBank'), data['account_number'], data.get('iban', ''), data.get('bic', '') )) beneficiary = cur.fetchone() # Log d'audit cur.execute(''' INSERT INTO audit_logs (user_id, action, details, ip_address) VALUES (%s, %s, %s, %s) ''', ( current_user_id, 'ADD_BENEFICIARY', f'{{"beneficiary_name": "{data["beneficiary_name"]}"}}', request.remote_addr )) conn.commit() return jsonify({ 'message': 'Bénéficiaire ajouté avec succès', 'beneficiary': serialize_row(beneficiary) }), 201 except Exception as e: conn.rollback() return jsonify({'error': f'Erreur: {str(e)}'}), 500 finally: conn.close() @app.route('/api/beneficiaries/', methods=['DELETE']) @token_required def delete_beneficiary(current_user_id, beneficiary_id): """Suppression d'un bénéficiaire.""" conn = get_db_connection() try: cur = conn.cursor() cur.execute(''' DELETE FROM beneficiaries WHERE id = %s AND user_id = %s RETURNING id ''', (beneficiary_id, current_user_id)) deleted = cur.fetchone() if not deleted: return jsonify({'error': 'Bénéficiaire non trouvé'}), 404 conn.commit() return jsonify({'message': 'Bénéficiaire supprimé'}), 200 except Exception as e: conn.rollback() return jsonify({'error': str(e)}), 500 finally: conn.close() # ============================================ # ROUTES - VIREMENTS / TRANSACTIONS # ============================================ @app.route('/api/transfers/internal', methods=['POST']) @token_required def transfer_internal(current_user_id): """ Virement interne entre ses propres comptes. """ data = request.get_json() required_fields = ['from_account_id', 'to_account_id', 'amount'] for field in required_fields: if field not in data: return jsonify({'error': f'Le champ {field} est requis'}), 400 amount = float(data['amount']) if amount <= 0: return jsonify({'error': 'Le montant doit être positif'}), 400 if data['from_account_id'] == data['to_account_id']: return jsonify({'error': 'Les comptes source et destination doivent être différents'}), 400 conn = get_db_connection() try: cur = conn.cursor() # Vérifier compte source cur.execute(''' SELECT id, balance, account_number, account_type FROM accounts WHERE id = %s AND user_id = %s AND status = 'active' ''', (data['from_account_id'], current_user_id)) from_account = cur.fetchone() if not from_account: return jsonify({'error': 'Compte source non trouvé'}), 404 if float(from_account['balance']) < amount: return jsonify({'error': 'Solde insuffisant'}), 400 # Vérifier compte destination cur.execute(''' SELECT id, account_number, account_type FROM accounts WHERE id = %s AND user_id = %s AND status = 'active' ''', (data['to_account_id'], current_user_id)) to_account = cur.fetchone() if not to_account: return jsonify({'error': 'Compte destination non trouvé'}), 404 # Effectuer le virement cur.execute( 'UPDATE accounts SET balance = balance - %s WHERE id = %s', (amount, data['from_account_id']) ) cur.execute( 'UPDATE accounts SET balance = balance + %s WHERE id = %s', (amount, data['to_account_id']) ) # Enregistrer la transaction description = data.get('description', f"Virement interne {from_account['account_type']} → {to_account['account_type']}") cur.execute(''' INSERT INTO transactions (from_account_id, to_account_id, transaction_type, amount, description, status, executed_at) VALUES (%s, %s, 'virement_interne', %s, %s, 'completed', NOW()) RETURNING id, amount, description, status, created_at ''', (data['from_account_id'], data['to_account_id'], amount, description)) transaction = cur.fetchone() # Log d'audit cur.execute(''' INSERT INTO audit_logs (user_id, action, details, ip_address) VALUES (%s, %s, %s, %s) ''', ( current_user_id, 'TRANSFER_INTERNAL', f'{{"amount": {amount}, "from": "{from_account["account_number"]}", "to": "{to_account["account_number"]}"}}', request.remote_addr )) conn.commit() return jsonify({ 'message': 'Virement interne effectué avec succès', 'transaction': serialize_row(transaction) }), 200 except Exception as e: conn.rollback() return jsonify({'error': f'Erreur lors du virement: {str(e)}'}), 500 finally: conn.close() @app.route('/api/transfers/person', methods=['POST']) @token_required def transfer_to_person(current_user_id): """ Virement vers une autre personne (bénéficiaire) dans DragonBank. """ data = request.get_json() required_fields = ['from_account_id', 'beneficiary_id', 'amount'] for field in required_fields: if field not in data: return jsonify({'error': f'Le champ {field} est requis'}), 400 amount = float(data['amount']) if amount <= 0: return jsonify({'error': 'Le montant doit être positif'}), 400 conn = get_db_connection() try: cur = conn.cursor() # Vérifier compte source cur.execute(''' SELECT id, balance, account_number FROM accounts WHERE id = %s AND user_id = %s AND status = 'active' ''', (data['from_account_id'], current_user_id)) from_account = cur.fetchone() if not from_account: return jsonify({'error': 'Compte source non trouvé'}), 404 if float(from_account['balance']) < amount: return jsonify({'error': 'Solde insuffisant'}), 400 # Vérifier le bénéficiaire cur.execute(''' SELECT id, beneficiary_name, account_number, bank_name FROM beneficiaries WHERE id = %s AND user_id = %s AND status = 'approved' ''', (data['beneficiary_id'], current_user_id)) beneficiary = cur.fetchone() if not beneficiary: return jsonify({'error': 'Bénéficiaire non trouvé ou non approuvé'}), 404 # Chercher le compte destination dans DragonBank cur.execute(''' SELECT id, account_number FROM accounts WHERE account_number = %s AND status = 'active' ''', (beneficiary['account_number'],)) to_account = cur.fetchone() if not to_account: return jsonify({'error': 'Compte bénéficiaire introuvable dans DragonBank'}), 404 # Effectuer le virement cur.execute( 'UPDATE accounts SET balance = balance - %s WHERE id = %s', (amount, data['from_account_id']) ) cur.execute( 'UPDATE accounts SET balance = balance + %s WHERE id = %s', (amount, str(to_account['id'])) ) description = data.get('description', f"Virement à {beneficiary['beneficiary_name']}") cur.execute(''' INSERT INTO transactions (from_account_id, to_account_id, transaction_type, amount, description, status, executed_at) VALUES (%s, %s, 'virement_entre_personnes', %s, %s, 'completed', NOW()) RETURNING id, amount, description, status, created_at ''', (data['from_account_id'], str(to_account['id']), amount, description)) transaction = cur.fetchone() # Log d'audit cur.execute(''' INSERT INTO audit_logs (user_id, action, details, ip_address) VALUES (%s, %s, %s, %s) ''', ( current_user_id, 'TRANSFER_PERSON', f'{{"amount": {amount}, "beneficiary": "{beneficiary["beneficiary_name"]}"}}', request.remote_addr )) conn.commit() return jsonify({ 'message': f'Virement de {amount}€ à {beneficiary["beneficiary_name"]} effectué', 'transaction': serialize_row(transaction) }), 200 except Exception as e: conn.rollback() return jsonify({'error': f'Erreur lors du virement: {str(e)}'}), 500 finally: conn.close() @app.route('/api/transfers/external', methods=['POST']) @token_required def transfer_external(current_user_id): """ Virement depuis/vers une autre banque (simulé). """ data = request.get_json() required_fields = ['account_id', 'amount', 'external_bank_name', 'external_account_number', 'direction'] for field in required_fields: if field not in data: return jsonify({'error': f'Le champ {field} est requis'}), 400 amount = float(data['amount']) if amount <= 0: return jsonify({'error': 'Le montant doit être positif'}), 400 direction = data['direction'] # 'incoming' ou 'outgoing' conn = get_db_connection() try: cur = conn.cursor() # Vérifier le compte cur.execute(''' SELECT id, balance, account_number FROM accounts WHERE id = %s AND user_id = %s AND status = 'active' ''', (data['account_id'], current_user_id)) account = cur.fetchone() if not account: return jsonify({'error': 'Compte non trouvé'}), 404 if direction == 'outgoing' and float(account['balance']) < amount: return jsonify({'error': 'Solde insuffisant'}), 400 # Effectuer l'opération if direction == 'outgoing': cur.execute( 'UPDATE accounts SET balance = balance - %s WHERE id = %s', (amount, data['account_id']) ) description = f"Virement sortant vers {data['external_bank_name']} - {data['external_account_number']}" from_id = data['account_id'] to_id = None else: # incoming cur.execute( 'UPDATE accounts SET balance = balance + %s WHERE id = %s', (amount, data['account_id']) ) description = f"Virement entrant depuis {data['external_bank_name']} - {data['external_account_number']}" from_id = None to_id = data['account_id'] description = data.get('description', description) cur.execute(''' INSERT INTO transactions (from_account_id, to_account_id, transaction_type, amount, description, status, external_bank_name, external_account_number, executed_at) VALUES (%s, %s, 'virement_externe', %s, %s, 'completed', %s, %s, NOW()) RETURNING id, amount, description, status, created_at ''', (from_id, to_id, amount, description, data['external_bank_name'], data['external_account_number'])) transaction = cur.fetchone() # Log d'audit cur.execute(''' INSERT INTO audit_logs (user_id, action, details, ip_address) VALUES (%s, %s, %s, %s) ''', ( current_user_id, 'TRANSFER_EXTERNAL', f'{{"amount": {amount}, "direction": "{direction}", "bank": "{data["external_bank_name"]}"}}', request.remote_addr )) conn.commit() return jsonify({ 'message': f'Virement externe {"sortant" if direction == "outgoing" else "entrant"} de {amount}€ effectué', 'transaction': serialize_row(transaction) }), 200 except Exception as e: conn.rollback() return jsonify({'error': f'Erreur: {str(e)}'}), 500 finally: conn.close() # ============================================ # ROUTES - HISTORIQUE DES TRANSACTIONS # ============================================ @app.route('/api/transactions', methods=['GET']) @token_required def get_transactions(current_user_id): """Récupère l'historique des transactions de l'utilisateur.""" limit = request.args.get('limit', 50, type=int) account_id = request.args.get('account_id', None) conn = get_db_connection() try: cur = conn.cursor() if account_id: cur.execute(''' SELECT t.id, t.from_account_id, t.to_account_id, t.transaction_type, t.amount, t.currency, t.description, t.status, t.reference, t.external_bank_name, t.external_account_number, t.created_at, t.executed_at, fa.account_number as from_account_number, ta.account_number as to_account_number FROM transactions t LEFT JOIN accounts fa ON t.from_account_id = fa.id LEFT JOIN accounts ta ON t.to_account_id = ta.id WHERE (t.from_account_id = %s OR t.to_account_id = %s) ORDER BY t.created_at DESC LIMIT %s ''', (account_id, account_id, limit)) else: cur.execute(''' SELECT t.id, t.from_account_id, t.to_account_id, t.transaction_type, t.amount, t.currency, t.description, t.status, t.reference, t.external_bank_name, t.external_account_number, t.created_at, t.executed_at, fa.account_number as from_account_number, ta.account_number as to_account_number FROM transactions t LEFT JOIN accounts fa ON t.from_account_id = fa.id LEFT JOIN accounts ta ON t.to_account_id = ta.id WHERE fa.user_id = %s OR ta.user_id = %s ORDER BY t.created_at DESC LIMIT %s ''', (current_user_id, current_user_id, limit)) transactions = cur.fetchall() return jsonify({'transactions': serialize_rows(transactions)}), 200 finally: conn.close() # ============================================ # ROUTES - STATISTIQUES # ============================================ @app.route('/api/stats', methods=['GET']) @token_required def get_stats(current_user_id): """Statistiques du compte utilisateur.""" conn = get_db_connection() try: cur = conn.cursor() # Solde total cur.execute(''' SELECT COALESCE(SUM(balance), 0) as total_balance, COUNT(*) as total_accounts FROM accounts WHERE user_id = %s AND status = 'active' ''', (current_user_id,)) balance_stats = cur.fetchone() # Nombre de transactions ce mois cur.execute(''' SELECT COUNT(*) as monthly_transactions, COALESCE(SUM(CASE WHEN fa.user_id = %s THEN amount ELSE 0 END), 0) as total_sent, COALESCE(SUM(CASE WHEN ta.user_id = %s THEN amount ELSE 0 END), 0) as total_received FROM transactions t LEFT JOIN accounts fa ON t.from_account_id = fa.id LEFT JOIN accounts ta ON t.to_account_id = ta.id WHERE (fa.user_id = %s OR ta.user_id = %s) AND t.created_at >= date_trunc('month', CURRENT_DATE) AND t.status = 'completed' ''', (current_user_id, current_user_id, current_user_id, current_user_id)) transaction_stats = cur.fetchone() # Nombre de bénéficiaires cur.execute(''' SELECT COUNT(*) as total_beneficiaries FROM beneficiaries WHERE user_id = %s ''', (current_user_id,)) ben_stats = cur.fetchone() return jsonify({ 'stats': { 'total_balance': decimal_to_float(balance_stats['total_balance']), 'total_accounts': balance_stats['total_accounts'], 'monthly_transactions': transaction_stats['monthly_transactions'], 'total_sent': decimal_to_float(transaction_stats['total_sent']), 'total_received': decimal_to_float(transaction_stats['total_received']), 'total_beneficiaries': ben_stats['total_beneficiaries'] } }), 200 finally: conn.close() # ============================================ # DÉMARRAGE # ============================================ if __name__ == '__main__': print("🐉 DragonBank Backend API starting...") print(f"📡 Database: {DATABASE_URL.split('@')[1] if '@' in DATABASE_URL else 'configured'}") app.run(host='0.0.0.0', port=5000, debug=False)