Files

946 lines
33 KiB
Python
Raw Permalink Normal View History

2026-03-27 10:20:35 +01:00
"""
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/<account_id>', 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/<beneficiary_id>', 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)