maj
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user