This commit is contained in:
2026-03-27 17:52:41 +01:00
parent 3cf8233054
commit 8320738acb
32 changed files with 5113 additions and 1385 deletions
@@ -0,0 +1,83 @@
from database import creer_connexion, enregistrer_audit
from auth import generer_token, obtenir_ip_client
from validators import valider_champs_obligatoires, valider_email, valider_mot_de_passe
from models.utilisateur import UtilisateurDAO
from models.compte import CompteDAO
class AuthService:
def __init__(self):
self.utilisateur_dao = UtilisateurDAO()
self.compte_dao = CompteDAO()
def inscrire(self, donnees):
conn = creer_connexion()
try:
valider_champs_obligatoires(donnees, ['email', 'password', 'first_name', 'last_name'])
email = valider_email(donnees['email'])
valider_mot_de_passe(donnees['password'])
if self.utilisateur_dao.email_existe(conn, email):
raise ValueError('Cette adresse email est deja associee a un compte')
utilisateur = self.utilisateur_dao.creer(
conn, email, donnees['password'],
donnees['first_name'].strip(),
donnees['last_name'].strip(),
donnees.get('phone', '').strip() or None,
donnees.get('address', '').strip() or None,
donnees.get('date_of_birth') or None
)
compte = self.compte_dao.ouvrir(conn, utilisateur['id'], 'courant')
enregistrer_audit(conn.cursor(), utilisateur['id'], 'REGISTER', {
'email': email, 'account_number': compte['account_number']
}, obtenir_ip_client())
conn.commit()
token = generer_token(utilisateur['id'], email)
return {
'message': 'Inscription reussie ! Bienvenue chez DragonBank.',
'user': utilisateur,
'token': token,
'account_number': compte['account_number']
}
except Exception:
conn.rollback()
raise
finally:
conn.close()
def connecter(self, donnees):
if not donnees or not donnees.get('email') or not donnees.get('password'):
raise ValueError('Email et mot de passe requis')
conn = creer_connexion()
try:
email = donnees['email'].strip().lower()
utilisateur = self.utilisateur_dao.authentifier(conn, email, donnees['password'])
if not utilisateur:
raise PermissionError('Email ou mot de passe incorrect')
self.utilisateur_dao.mettre_a_jour_derniere_connexion(conn, str(utilisateur['id']))
enregistrer_audit(conn.cursor(), str(utilisateur['id']), 'LOGIN',
{'ip': obtenir_ip_client()}, obtenir_ip_client())
conn.commit()
return {
'message': 'Connexion reussie',
'token': generer_token(utilisateur['id'], utilisateur['email']),
'user': {
'id': str(utilisateur['id']),
'email': utilisateur['email'],
'first_name': utilisateur['first_name'],
'last_name': utilisateur['last_name']
}
}
except Exception:
conn.rollback()
raise
finally:
conn.close()
@@ -0,0 +1,54 @@
from database import creer_connexion, enregistrer_audit
from auth import obtenir_ip_client
from models.beneficiaire import BeneficiaireDAO
from validators import valider_champs_obligatoires
class BeneficiaireService:
def __init__(self):
self.beneficiaire_dao = BeneficiaireDAO()
def obtenir_beneficiaires(self, id_utilisateur_courant):
conn = creer_connexion()
try:
return self.beneficiaire_dao.lister_par_utilisateur(conn, id_utilisateur_courant)
finally:
conn.close()
def ajouter_beneficiaire(self, id_utilisateur_courant, donnees):
conn = creer_connexion()
try:
valider_champs_obligatoires(donnees, ['beneficiary_name', 'account_number'])
beneficiaire = self.beneficiaire_dao.ajouter(
conn,
id_utilisateur_courant,
donnees['beneficiary_name'].strip(),
donnees['account_number'].strip().upper(),
donnees.get('bank_name', 'DragonBank').strip(),
donnees.get('iban', '').strip().upper() or None,
donnees.get('bic', '').strip().upper() or None
)
enregistrer_audit(conn.cursor(), id_utilisateur_courant, 'ADD_BENEFICIARY',
{'nom': donnees['beneficiary_name']}, obtenir_ip_client())
conn.commit()
return beneficiaire
except Exception:
conn.rollback()
raise
finally:
conn.close()
def supprimer_beneficiaire(self, id_utilisateur_courant, id_beneficiaire):
conn = creer_connexion()
try:
supprime = self.beneficiaire_dao.supprimer(conn, id_beneficiaire, id_utilisateur_courant)
if not supprime:
raise ValueError('Beneficiaire introuvable')
enregistrer_audit(conn.cursor(), id_utilisateur_courant, 'DELETE_BENEFICIARY',
{'id': id_beneficiaire}, obtenir_ip_client())
conn.commit()
return True
except Exception:
conn.rollback()
raise
finally:
conn.close()
@@ -0,0 +1,68 @@
from database import creer_connexion, enregistrer_audit
from auth import obtenir_ip_client
from models.compte import CompteDAO
from models.transaction import TransactionDAO
class CompteService:
def __init__(self):
self.compte_dao = CompteDAO()
self.transaction_dao = TransactionDAO()
def obtenir_comptes(self, id_utilisateur_courant):
conn = creer_connexion()
try:
return self.compte_dao.lister_par_utilisateur(conn, id_utilisateur_courant)
finally:
conn.close()
def ouvrir_compte(self, id_utilisateur_courant, donnees):
donnees = donnees or {}
conn = creer_connexion()
try:
import decimal as _d
depot_brut = donnees.get('initial_deposit')
depot = _d.Decimal(str(depot_brut)) if depot_brut else _d.Decimal('0')
if depot < 0:
raise ValueError('Le depot initial ne peut pas etre negatif')
compte = self.compte_dao.ouvrir(
conn, id_utilisateur_courant,
donnees.get('account_type', 'courant'),
depot
)
enregistrer_audit(conn.cursor(), id_utilisateur_courant, 'OPEN_ACCOUNT',
{'type': donnees.get('account_type'), 'depot': float(depot)},
obtenir_ip_client())
conn.commit()
return compte
except Exception:
conn.rollback()
raise
finally:
conn.close()
def obtenir_detail_compte(self, id_utilisateur_courant, id_compte):
conn = creer_connexion()
try:
compte = self.compte_dao.trouver_par_id(conn, id_compte, id_utilisateur_courant)
if not compte:
raise ValueError('Compte introuvable')
return compte
finally:
conn.close()
def historique_solde(self, id_utilisateur_courant, id_compte):
conn = creer_connexion()
try:
compte = self.compte_dao.trouver_par_id(conn, id_compte, id_utilisateur_courant)
if not compte:
raise ValueError('Compte introuvable')
points = self.transaction_dao.historique_solde(conn, id_compte, compte['balance'])
return {
'account_type': compte['account_type'],
'solde_actuel': compte['balance'],
'points': points
}
finally:
conn.close()
@@ -0,0 +1,41 @@
from database import creer_connexion, serialiser_valeur
from models.transaction import TransactionDAO
class StatsService:
def __init__(self):
self.transaction_dao = TransactionDAO()
def obtenir_statistiques(self, id_utilisateur_courant):
conn = creer_connexion()
try:
curseur = conn.cursor()
curseur.execute(
"""
SELECT COALESCE(SUM(balance), 0) AS total_balance,
COUNT(*) AS total_accounts
FROM accounts
WHERE user_id = %s AND status = 'active'
""",
(id_utilisateur_courant,)
)
stats_comptes = curseur.fetchone()
stats_tx = self.transaction_dao.statistiques(conn, id_utilisateur_courant)
curseur.execute(
'SELECT COUNT(*) AS total_beneficiaries FROM beneficiaries WHERE user_id = %s',
(id_utilisateur_courant,)
)
stats_ben = curseur.fetchone()
return {
'total_balance': serialiser_valeur(stats_comptes['total_balance']),
'total_accounts': stats_comptes['total_accounts'],
'monthly_transactions': stats_tx['transactions_mois'],
'total_sent': serialiser_valeur(stats_tx['total_envoye']),
'total_received': serialiser_valeur(stats_tx['total_recu']),
'total_beneficiaries': stats_ben['total_beneficiaries']
}
finally:
conn.close()
@@ -0,0 +1,147 @@
from database import creer_connexion, enregistrer_audit
from auth import obtenir_ip_client
from validators import valider_champs_obligatoires, valider_montant
from models.transaction import TransactionDAO
from models.compte import CompteDAO
from models.beneficiaire import BeneficiaireDAO
class TransactionService:
def __init__(self):
self.transaction_dao = TransactionDAO()
self.compte_dao = CompteDAO()
self.beneficiaire_dao = BeneficiaireDAO()
def virement_interne(self, id_utilisateur_courant, donnees):
conn = creer_connexion()
try:
valider_champs_obligatoires(donnees, ['from_account_id', 'to_account_id', 'amount'])
montant = valider_montant(donnees['amount'])
if donnees['from_account_id'] == donnees['to_account_id']:
raise ValueError('Les comptes source et destination doivent etre differents')
source = self.compte_dao.trouver_actif(conn, donnees['from_account_id'], id_utilisateur_courant)
dest = self.compte_dao.trouver_actif(conn, donnees['to_account_id'], id_utilisateur_courant)
if not source:
raise ValueError('Compte source introuvable')
if not dest:
raise ValueError('Compte destination introuvable')
libelle = donnees.get('description') or (
'Virement ' + source['account_type'] + ' vers ' + dest['account_type']
)
transaction = self.transaction_dao.virement_interne(
conn, donnees['from_account_id'], donnees['to_account_id'],
montant, libelle, self.compte_dao
)
enregistrer_audit(conn.cursor(), id_utilisateur_courant, 'TRANSFER_INTERNAL',
{'montant': float(montant), 'source': source['account_number'],
'dest': dest['account_number']}, obtenir_ip_client())
conn.commit()
return transaction
except Exception:
conn.rollback()
raise
finally:
conn.close()
def virement_beneficiaire(self, id_utilisateur_courant, donnees):
conn = creer_connexion()
try:
valider_champs_obligatoires(donnees, ['from_account_id', 'beneficiary_id', 'amount'])
montant = valider_montant(donnees['amount'])
source = self.compte_dao.trouver_actif(conn, donnees['from_account_id'], id_utilisateur_courant)
if not source:
raise ValueError('Compte source introuvable')
beneficiaire = self.beneficiaire_dao.trouver_approuve(
conn, donnees['beneficiary_id'], id_utilisateur_courant
)
if not beneficiaire:
raise ValueError('Beneficiaire introuvable ou non approuve')
dest = self.compte_dao.trouver_par_numero(conn, beneficiaire['account_number'])
if not dest:
raise ValueError('Le compte du beneficiaire est introuvable dans DragonBank')
libelle = donnees.get('description') or 'Virement a ' + beneficiaire['beneficiary_name']
transaction = self.transaction_dao.virement_entre_personnes(
conn, donnees['from_account_id'], str(dest['id']),
montant, libelle, self.compte_dao
)
enregistrer_audit(conn.cursor(), id_utilisateur_courant, 'TRANSFER_PERSON',
{'montant': float(montant), 'beneficiaire': beneficiaire['beneficiary_name']},
obtenir_ip_client())
conn.commit()
return transaction
except Exception:
conn.rollback()
raise
finally:
conn.close()
def virement_externe(self, id_utilisateur_courant, donnees):
conn = creer_connexion()
try:
valider_champs_obligatoires(
donnees,
['account_id', 'amount', 'external_bank_name', 'external_account_number', 'direction']
)
direction = donnees['direction']
if direction not in ('incoming', 'outgoing'):
raise ValueError('La direction doit etre "incoming" ou "outgoing"')
montant = valider_montant(donnees['amount'])
compte = self.compte_dao.trouver_actif(conn, donnees['account_id'], id_utilisateur_courant)
if not compte:
raise ValueError('Compte introuvable')
nom_banque = donnees['external_bank_name'].strip()
num_ext = donnees['external_account_number'].strip().upper()
libelle = donnees.get('description', '').strip() or (
'Virement ' + ('sortant vers ' if direction == 'outgoing' else 'entrant depuis ')
+ nom_banque + ' (' + num_ext + ')'
)
transaction = self.transaction_dao.virement_externe(
conn, donnees['account_id'], montant, direction,
nom_banque, num_ext, libelle, self.compte_dao
)
enregistrer_audit(conn.cursor(), id_utilisateur_courant, 'TRANSFER_EXTERNAL',
{'montant': float(montant), 'direction': direction, 'banque': nom_banque},
obtenir_ip_client())
conn.commit()
return transaction, direction
except Exception:
conn.rollback()
raise
finally:
conn.close()
def obtenir_transactions(self, id_utilisateur_courant, id_compte_filtre, type_tx_filtre, limite):
conn = creer_connexion()
try:
if id_compte_filtre:
if not self.compte_dao.trouver_par_id(conn, id_compte_filtre, id_utilisateur_courant):
raise ValueError('Compte introuvable ou acces refuse')
transactions = self.transaction_dao.lister(
conn, id_utilisateur_courant, id_compte_filtre, type_tx_filtre, limite
)
return transactions
finally:
conn.close()
def exporter_csv(self, id_utilisateur_courant, id_compte_filtre):
conn = creer_connexion()
try:
if id_compte_filtre:
if not self.compte_dao.trouver_par_id(conn, id_compte_filtre, id_utilisateur_courant):
raise ValueError('Compte introuvable ou acces refuse')
return self.transaction_dao.exporter_csv(conn, id_utilisateur_courant, id_compte_filtre)
finally:
conn.close()
@@ -0,0 +1,133 @@
"""
DragonBank - Couche Service (Utilisateur)
=========================================
Ce module implémente le pattern 'Service Layer' (Couche Métier).
Choix architecturaux :
1. Séparation des responsabilités (Separation of Concerns) :
Les routes Flask (Controllers) ne font que recevoir les requêtes HTTP.
C'est cette classe Service qui concentre TOUTES les règles métier complexes
(validation des mots de passe bcrypt, règles de profil, audits de sécurité).
2. Gestion Transactionnelle (ACID) :
Chaque méthode du service encapsule l'ouverture et la fermeture de la connexion DB.
Si une exception survient pendant une mise à jour, le bloc `except` appelle un `conn.rollback()`
pour garantir que la base de données ne reste jamais dans un état instable.
3. Abstraction des Données (DAO) :
Le service instancie la classe `UtilisateurDAO` pour lire et écrire les données.
Il ignore totalement la complexité des requêtes SQL (Pattern Repository/DAO).
"""
from database import creer_connexion, enregistrer_audit
from auth import obtenir_ip_client, verifier_mot_de_passe, hacher_mot_de_passe
from config import CHAMPS_PROFIL_MODIFIABLES
from validators import valider_email, valider_mot_de_passe
from models.utilisateur import UtilisateurDAO
class UtilisateurService:
def __init__(self):
self.utilisateur_dao = UtilisateurDAO()
def obtenir_profil(self, id_utilisateur_courant):
conn = creer_connexion()
try:
utilisateur = self.utilisateur_dao.trouver_par_id(conn, id_utilisateur_courant)
if not utilisateur:
raise ValueError('Utilisateur introuvable')
return utilisateur
finally:
conn.close()
def modifier_profil(self, id_utilisateur_courant, donnees):
donnees = donnees or {}
champs = {
k: v.strip()
for k, v in donnees.items()
if k in CHAMPS_PROFIL_MODIFIABLES and isinstance(v, str)
}
if not champs:
raise ValueError('Aucun champ valide a mettre a jour')
conn = creer_connexion()
try:
mis_a_jour = self.utilisateur_dao.mettre_a_jour_profil(conn, id_utilisateur_courant, champs)
enregistrer_audit(conn.cursor(), id_utilisateur_courant, 'UPDATE_PROFILE',
{'champs': list(champs.keys())}, obtenir_ip_client())
conn.commit()
return mis_a_jour
except Exception:
conn.rollback()
raise
finally:
conn.close()
def changer_mot_de_passe(self, id_utilisateur_courant, donnees):
donnees = donnees or {}
ancien_mot_de_passe = donnees.get('current_password')
nouveau_mot_de_passe = donnees.get('new_password')
if not ancien_mot_de_passe or not nouveau_mot_de_passe:
raise ValueError('Le mot de passe actuel et le nouveau mot de passe sont requis')
valider_mot_de_passe(nouveau_mot_de_passe)
conn = creer_connexion()
try:
utilisateur = self.utilisateur_dao.trouver_par_id(conn, id_utilisateur_courant)
if not utilisateur:
raise ValueError('Utilisateur introuvable')
utilisateur_complet = self.utilisateur_dao.trouver_par_email(conn, utilisateur['email'])
if not verifier_mot_de_passe(ancien_mot_de_passe, utilisateur_complet['password_hash']):
raise PermissionError('Le mot de passe actuel est incorrect')
nouveau_hash = hacher_mot_de_passe(nouveau_mot_de_passe)
self.utilisateur_dao.mettre_a_jour_mot_de_passe(conn, id_utilisateur_courant, nouveau_hash)
enregistrer_audit(conn.cursor(), id_utilisateur_courant, 'UPDATE_PASSWORD', {}, obtenir_ip_client())
conn.commit()
return True
except Exception:
conn.rollback()
raise
finally:
conn.close()
def changer_email(self, id_utilisateur_courant, donnees):
donnees = donnees or {}
mot_de_passe = donnees.get('password')
nouvel_email = donnees.get('new_email')
if not mot_de_passe or not nouvel_email:
raise ValueError('Le mot de passe et le nouvel email sont requis')
email_valide = valider_email(nouvel_email)
conn = creer_connexion()
try:
utilisateur = self.utilisateur_dao.trouver_par_id(conn, id_utilisateur_courant)
if not utilisateur:
raise ValueError('Utilisateur introuvable')
if utilisateur['email'] == email_valide:
raise ValueError("Le nouvel email est identique a l'actuel")
if self.utilisateur_dao.email_existe(conn, email_valide):
raise ValueError('Cette adresse email est deja utilisee')
utilisateur_complet = self.utilisateur_dao.trouver_par_email(conn, utilisateur['email'])
if not verifier_mot_de_passe(mot_de_passe, utilisateur_complet['password_hash']):
raise PermissionError('Mot de passe incorrect')
self.utilisateur_dao.mettre_a_jour_email(conn, id_utilisateur_courant, email_valide)
enregistrer_audit(conn.cursor(), id_utilisateur_courant, 'UPDATE_EMAIL',
{'ancien': utilisateur['email'], 'nouveau': email_valide},
obtenir_ip_client())
conn.commit()
return True
except Exception:
conn.rollback()
raise
finally:
conn.close()