Refactoring - Version stable

This commit is contained in:
dahoud
2026-03-28 14:21:30 +00:00
parent 00b981c510
commit a740c172ef
4402 changed files with 88517 additions and 1555 deletions

View File

@@ -219,15 +219,25 @@ public class UnionFlowServerApplication implements QuarkusApplication {
LOG.info("--------------------------------------------------------------");
}
/**
* Retourne la valeur de la variable d'environnement UNIONFLOW_DOMAIN.
* Méthode protégée pour permettre la substitution en tests.
*
* @return valeur de UNIONFLOW_DOMAIN, ou null si non définie
*/
protected String getUnionflowDomain() {
return System.getenv("UNIONFLOW_DOMAIN");
}
/**
* Construit l'URL de base de l'application.
*
* @return URL complète (ex: http://localhost:8080)
*/
private String buildBaseUrl() {
String buildBaseUrl() {
// En production, utiliser le nom de domaine configuré
if ("prod".equals(activeProfile)) {
String domain = System.getenv("UNIONFLOW_DOMAIN");
String domain = getUnionflowDomain();
if (domain != null && !domain.isEmpty()) {
return "https://" + domain;
}

View File

@@ -43,30 +43,27 @@ public class OidcTokenPropagationHeadersFactory implements ClientHeadersFactory
}
// STRATÉGIE 2 : Récupérer depuis SecurityIdentity
if (securityIdentity.isResolvable()) {
SecurityIdentity identity = securityIdentity.get();
// En contexte CDI, securityIdentity.isResolvable() est toujours true.
SecurityIdentity identity = securityIdentity.get();
if (identity != null && !identity.isAnonymous()) {
if (identity.getPrincipal() instanceof OidcJwtCallerPrincipal) {
OidcJwtCallerPrincipal principal = (OidcJwtCallerPrincipal) identity.getPrincipal();
String token = principal.getRawToken();
if (identity != null && !identity.isAnonymous()) {
if (identity.getPrincipal() instanceof OidcJwtCallerPrincipal) {
OidcJwtCallerPrincipal principal = (OidcJwtCallerPrincipal) identity.getPrincipal();
String token = principal.getRawToken();
if (token != null && !token.isBlank()) {
result.add("Authorization", "Bearer " + token);
LOG.infof("✅ Token JWT propagé depuis SecurityIdentity (longueur: %d)", token.length());
return result;
} else {
LOG.warnf("⚠️ Token JWT vide dans SecurityIdentity");
}
if (token != null && !token.isBlank()) {
result.add("Authorization", "Bearer " + token);
LOG.infof("✅ Token JWT propagé depuis SecurityIdentity (longueur: %d)", token.length());
return result;
} else {
LOG.warnf("⚠️ Principal n'est pas un OidcJwtCallerPrincipal (type: %s)",
identity.getPrincipal().getClass().getName());
LOG.warnf("⚠️ Token JWT vide dans SecurityIdentity");
}
} else {
LOG.warnf("⚠️ SecurityIdentity null ou utilisateur anonyme");
LOG.warnf("⚠️ Principal n'est pas un OidcJwtCallerPrincipal (type: %s)",
identity.getPrincipal() != null ? identity.getPrincipal().getClass().getName() : "null");
}
} else {
LOG.warnf("⚠️ SecurityIdentity non disponible dans le contexte");
LOG.warnf("⚠️ SecurityIdentity null ou utilisateur anonyme");
}
LOG.errorf("❌ Impossible de propager le token JWT - aucune stratégie n'a fonctionné");

View File

@@ -5,6 +5,7 @@ import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* Entité représentant une alerte LCB-FT (Lutte Contre le Blanchiment et Financement du Terrorisme).
@@ -99,6 +100,7 @@ public class AlerteLcbFt extends BaseEntity {
/**
* Indique si l'alerte a été traitée
*/
@Builder.Default
@Column(name = "traitee", nullable = false)
private Boolean traitee = false;
@@ -111,8 +113,8 @@ public class AlerteLcbFt extends BaseEntity {
/**
* Utilisateur ayant traité l'alerte
*/
@Column(name = "traite_par", length = 100)
private String traitePar;
@Column(name = "traite_par")
private UUID traitePar;
/**
* Commentaire sur le traitement

View File

@@ -90,6 +90,7 @@ public class AuditLog extends BaseEntity {
@PrePersist
protected void onCreate() {
super.onCreate();
if (dateHeure == null) {
dateHeure = LocalDateTime.now();
}

View File

@@ -15,13 +15,7 @@ import lombok.NoArgsConstructor;
* @version 1.0
*/
@Entity
@Table(
name = "configurations",
indexes = {
@Index(name = "idx_config_cle", columnList = "cle", unique = true),
@Index(name = "idx_config_categorie", columnList = "categorie")
}
)
@Table(name = "configurations")
@Data
@NoArgsConstructor
@AllArgsConstructor

View File

@@ -6,6 +6,7 @@ import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@@ -156,12 +157,15 @@ public class Cotisation extends BaseEntity {
return dateEcheance != null && dateEcheance.isBefore(LocalDate.now()) && !isEntierementPayee();
}
private static final AtomicLong REFERENCE_COUNTER =
new AtomicLong(System.currentTimeMillis() % 100000000L);
/** Méthode métier pour générer un numéro de référence unique */
public static String genererNumeroReference() {
return "COT-"
+ LocalDate.now().getYear()
+ "-"
+ String.format("%08d", System.currentTimeMillis() % 100000000);
+ String.format("%08d", REFERENCE_COUNTER.incrementAndGet() % 100000000L);
}
/** Callback JPA avant la persistance */

View File

@@ -4,6 +4,7 @@ import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.concurrent.atomic.AtomicLong;
import lombok.*;
/**
@@ -108,9 +109,12 @@ public class DemandeAdhesion extends BaseEntity {
&& montantPaye.compareTo(fraisAdhesion) >= 0;
}
private static final AtomicLong REFERENCE_COUNTER =
new AtomicLong(System.currentTimeMillis() % 100000000L);
public static String genererNumeroReference() {
return "ADH-" + java.time.LocalDate.now().getYear()
+ "-" + String.format("%08d", System.currentTimeMillis() % 100000000);
+ "-" + String.format("%08d", REFERENCE_COUNTER.incrementAndGet() % 100000000L);
}
@PrePersist

View File

@@ -5,6 +5,7 @@ import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.concurrent.atomic.AtomicLong;
import java.util.ArrayList;
import java.util.List;
import lombok.AllArgsConstructor;
@@ -114,12 +115,15 @@ public class Paiement extends BaseEntity {
@JoinColumn(name = "transaction_wave_id")
private TransactionWave transactionWave;
private static final AtomicLong REFERENCE_COUNTER =
new AtomicLong(System.currentTimeMillis() % 1000000000000L);
/** Méthode métier pour générer un numéro de référence unique */
public static String genererNumeroReference() {
return "PAY-"
+ LocalDateTime.now().getYear()
+ "-"
+ String.format("%012d", System.currentTimeMillis() % 1000000000000L);
+ String.format("%012d", REFERENCE_COUNTER.incrementAndGet() % 1000000000000L);
}
/** Méthode métier pour vérifier si le paiement est validé */

View File

@@ -33,21 +33,28 @@ public class GlobalExceptionMapper implements ExceptionMapper<Throwable> {
@Override
public Response toResponse(Throwable exception) {
// Logger l'exception dans les logs applicatifs
log.error("Unhandled exception", exception);
// Déterminer le code HTTP
int statusCode = determineStatusCode(exception);
// Récupérer l'endpoint (safe pour les tests unitaires)
String endpoint = "unknown";
try {
// Logger l'exception dans les logs applicatifs
log.error("Unhandled exception", exception);
if (uriInfo != null) {
endpoint = uriInfo.getPath();
}
} catch (Exception e) {
// Ignore - pas de contexte REST (ex: test unitaire)
}
// Déterminer le code HTTP
int statusCode = determineStatusCode(exception);
// Générer le message et le stacktrace
String message = exception.getMessage() != null ? exception.getMessage() : exception.getClass().getSimpleName();
String stacktrace = getStackTrace(exception);
// Récupérer l'endpoint
String endpoint = uriInfo != null ? uriInfo.getPath() : "unknown";
// Générer le message et le stacktrace
String message = exception.getMessage() != null ? exception.getMessage() : exception.getClass().getSimpleName();
String stacktrace = getStackTrace(exception);
// Persister dans system_logs
// Persister dans system_logs (ne pas laisser ça crasher le mapper)
try {
systemLoggingService.logError(
determineSource(exception),
message,
@@ -57,17 +64,12 @@ public class GlobalExceptionMapper implements ExceptionMapper<Throwable> {
"/" + endpoint,
statusCode
);
// Retourner une réponse HTTP appropriée
return buildErrorResponse(exception, statusCode);
} catch (Exception e) {
// Ne jamais laisser l'exception mapper lui-même crasher
log.error("Error in GlobalExceptionMapper", e);
return Response.serverError()
.entity(java.util.Map.of("error", "Internal server error"))
.build();
log.warn("Failed to log error to system_logs", e);
}
// Retourner une réponse HTTP appropriée
return buildErrorResponse(exception, statusCode);
}
private int determineStatusCode(Throwable exception) {

View File

@@ -0,0 +1,40 @@
package dev.lions.unionflow.server.exception;
import com.fasterxml.jackson.core.JsonProcessingException;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.Map;
/**
* Exception Mapper pour les erreurs de traitement JSON (parsing, format, etc.).
* Retourne un 400 Bad Request avec un message détaillé.
*
* @author UnionFlow Team
* @version 1.0
* @since 2026-03-19
*/
@Slf4j
@Provider
public class JsonProcessingExceptionMapper implements ExceptionMapper<JsonProcessingException> {
@Override
public Response toResponse(JsonProcessingException exception) {
log.warn("JSON processing error: {}", exception.getMessage());
Map<String, Object> errorBody = new HashMap<>();
errorBody.put("message", "Erreur de traitement JSON");
errorBody.put("details", exception.getOriginalMessage() != null
? exception.getOriginalMessage()
: exception.getMessage());
errorBody.put("status", 400);
errorBody.put("timestamp", java.time.LocalDateTime.now().toString());
return Response.status(Response.Status.BAD_REQUEST)
.entity(errorBody)
.build();
}
}

View File

@@ -473,8 +473,7 @@ public class CotisationRepository extends BaseRepository<Cotisation> {
public boolean incrementerNombreRappels(UUID cotisationId) {
Cotisation cotisation = findByIdOptional(cotisationId).orElse(null);
if (cotisation != null) {
cotisation.setNombreRappels(
cotisation.getNombreRappels() != null ? cotisation.getNombreRappels() + 1 : 1);
cotisation.setNombreRappels(cotisation.getNombreRappels() + 1);
cotisation.setDateDernierRappel(LocalDateTime.now());
update(cotisation);
return true;
@@ -552,7 +551,7 @@ public class CotisationRepository extends BaseRepository<Cotisation> {
"cotisationsPayees", cotisationsPayees != null ? cotisationsPayees : 0L,
"tauxPaiement",
totalCotisations != null && totalCotisations > 0
? (cotisationsPayees != null ? cotisationsPayees : 0L) * 100.0 / totalCotisations
? cotisationsPayees * 100.0 / totalCotisations
: 0.0);
}

View File

@@ -247,13 +247,12 @@ public class DemandeAideRepository extends BaseRepository<DemandeAide> {
query.setParameter("statut", StatutAide.APPROUVEE);
query.setParameter("debut", debut);
query.setParameter("fin", fin);
BigDecimal result = query.getSingleResult();
return result != null ? result : BigDecimal.ZERO;
return query.getSingleResult();
}
/** Construit la clause ORDER BY à partir d'un Sort */
private String buildOrderBy(Sort sort) {
if (sort == null || sort.getColumns().isEmpty()) {
if (sort.getColumns().isEmpty()) {
return "d.dateDemande DESC";
}
StringBuilder orderBy = new StringBuilder();

View File

@@ -213,8 +213,7 @@ public class EvenementRepository extends BaseRepository<Evenement> {
TypedQuery<Long> query = entityManager.createQuery(
"SELECT COUNT(e) FROM Evenement e WHERE e.organisation.id = :organisationId", Long.class);
query.setParameter("organisationId", organisationId);
Long result = query.getSingleResult();
return result != null ? result : 0L;
return query.getSingleResult();
}
/** Compte les événements actifs d'une organisation. */
@@ -224,8 +223,7 @@ public class EvenementRepository extends BaseRepository<Evenement> {
"SELECT COUNT(e) FROM Evenement e WHERE e.organisation.id = :organisationId AND (e.actif = true OR e.actif IS NULL)",
Long.class);
query.setParameter("organisationId", organisationId);
Long result = query.getSingleResult();
return result != null ? result : 0L;
return query.getSingleResult();
}
/** Événements à venir pour une organisation (pour dashboard par org). */
@@ -472,13 +470,12 @@ public class EvenementRepository extends BaseRepository<Evenement> {
query.setParameter("organisationId", organisationId);
query.setParameter("debut", debut);
query.setParameter("fin", fin);
Long result = query.getSingleResult();
return result != null ? result : 0L;
return query.getSingleResult();
}
/** Construit la clause ORDER BY à partir d'un Sort */
private String buildOrderBy(Sort sort) {
if (sort == null || sort.getColumns().isEmpty()) {
if (sort.getColumns().isEmpty()) {
return "e.dateDebut";
}
StringBuilder orderBy = new StringBuilder();

View File

@@ -112,8 +112,7 @@ public class MembreRepository extends BaseRepository<Membre> {
"SELECT COUNT(DISTINCT m) FROM Membre m JOIN m.membresOrganisations mo WHERE mo.organisation.id IN :organisationIds",
Long.class);
query.setParameter("organisationIds", organisationIds);
Long result = query.getSingleResult();
return result != null ? result : 0L;
return query.getSingleResult();
}
/** Compte les membres actifs distincts appartenant à au moins une des organisations données (pour dashboard par org). */
@@ -125,8 +124,7 @@ public class MembreRepository extends BaseRepository<Membre> {
"SELECT COUNT(DISTINCT m) FROM Membre m JOIN m.membresOrganisations mo WHERE mo.organisation.id IN :organisationIds AND (m.actif = true OR m.actif IS NULL)",
Long.class);
query.setParameter("organisationIds", organisationIds);
Long result = query.getSingleResult();
return result != null ? result : 0L;
return query.getSingleResult();
}
/**
@@ -169,8 +167,7 @@ public class MembreRepository extends BaseRepository<Membre> {
Long.class);
query.setParameter("organisationId", organisationId);
query.setParameter("depuis", depuis);
Long result = query.getSingleResult();
return result != null ? result : 0L;
return query.getSingleResult();
}
/** Compte les adhésions à une organisation dans une période (pour graphiques dashboard par org). */
@@ -182,8 +179,7 @@ public class MembreRepository extends BaseRepository<Membre> {
query.setParameter("organisationId", organisationId);
query.setParameter("start", start);
query.setParameter("end", end);
Long result = query.getSingleResult();
return result != null ? result : 0L;
return query.getSingleResult();
}
/** Trouve les membres par statut avec pagination */
@@ -266,7 +262,7 @@ public class MembreRepository extends BaseRepository<Membre> {
/** Construit la clause ORDER BY à partir d'un Sort */
private String buildOrderBy(Sort sort) {
if (sort == null || sort.getColumns().isEmpty()) {
if (sort.getColumns().isEmpty()) {
return "m.id";
}
StringBuilder orderBy = new StringBuilder();

View File

@@ -61,7 +61,7 @@ public class PaiementRepository implements PanacheRepositoryBase<Paiement, UUID>
* @return Liste des paiements
*/
public List<Paiement> findByStatut(StatutPaiement statut) {
return find("statutPaiement = ?1 AND actif = true", Sort.by("datePaiement", Sort.Direction.Descending), statut)
return find("statutPaiement = ?1 AND actif = true", Sort.by("datePaiement", Sort.Direction.Descending), statut.name())
.list();
}
@@ -72,7 +72,7 @@ public class PaiementRepository implements PanacheRepositoryBase<Paiement, UUID>
* @return Liste des paiements
*/
public List<Paiement> findByMethode(MethodePaiement methode) {
return find("methodePaiement = ?1 AND actif = true", Sort.by("datePaiement", Sort.Direction.Descending), methode)
return find("methodePaiement = ?1 AND actif = true", Sort.by("datePaiement", Sort.Direction.Descending), methode.name())
.list();
}
@@ -87,7 +87,7 @@ public class PaiementRepository implements PanacheRepositoryBase<Paiement, UUID>
return find(
"statutPaiement = ?1 AND dateValidation >= ?2 AND dateValidation <= ?3 AND actif = true",
Sort.by("dateValidation", Sort.Direction.Descending),
StatutPaiement.VALIDE,
StatutPaiement.VALIDE.name(),
dateDebut,
dateFin)
.list();

View File

@@ -26,14 +26,13 @@ public class DemandeCreditRepository extends BaseRepository<DemandeCredit> {
// On somme l'échéantier non encore payé pour les crédits décaissés ou en contentieux
TypedQuery<BigDecimal> query = entityManager.createQuery(
"SELECT SUM(e.capitalAmorti) FROM EcheanceCredit e " +
"SELECT COALESCE(SUM(e.capitalAmorti), 0) FROM EcheanceCredit e " +
"WHERE e.demandeCredit.membre.id = :mid " +
"AND e.demandeCredit.statut IN ('DECAISSEE', 'EN_CONTENTIEUX') " +
"AND e.statut != 'PAYEE'", BigDecimal.class);
query.setParameter("mid", membreId);
BigDecimal res = query.getSingleResult();
return res != null ? res : BigDecimal.ZERO;
return query.getSingleResult();
}
}

View File

@@ -113,7 +113,14 @@ public class AlerteLcbFtResource {
alerte.setTraitee(true);
alerte.setDateTraitement(LocalDateTime.now());
alerte.setTraitePar(body.get("traitePar"));
String traiteParStr = body.get("traitePar");
if (traiteParStr != null && !traiteParStr.isBlank()) {
try {
alerte.setTraitePar(UUID.fromString(traiteParStr));
} catch (IllegalArgumentException e) {
throw new BadRequestException("traitePar doit être un UUID valide");
}
}
alerte.setCommentaireTraitement(body.get("commentaire"));
alerteLcbFtRepository.persist(alerte);
@@ -154,7 +161,7 @@ public class AlerteLcbFtResource {
.severite(alerte.getSeverite())
.traitee(alerte.getTraitee())
.dateTraitement(alerte.getDateTraitement())
.traitePar(alerte.getTraitePar())
.traitePar(alerte.getTraitePar() != null ? alerte.getTraitePar().toString() : null)
.commentaireTraitement(alerte.getCommentaireTraitement())
.build();
}

View File

@@ -291,25 +291,10 @@ public class AnalyticsResource {
description = "Récupère la liste de tous les types de métriques disponibles")
@APIResponse(responseCode = "200", description = "Types de métriques récupérés avec succès")
public Response obtenirTypesMetriques() {
try {
log.info("Récupération des types de métriques disponibles");
TypeMetrique[] typesMetriques = TypeMetrique.values();
return Response.ok(Map.of("typesMetriques", typesMetriques, "total", typesMetriques.length))
.build();
} catch (Exception e) {
log.error("Erreur lors de la récupération des types de métriques: {}", e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(
Map.of(
"error",
"Erreur lors de la récupération des types de métriques",
"message",
e.getMessage()))
.build();
}
log.info("Récupération des types de métriques disponibles");
TypeMetrique[] typesMetriques = TypeMetrique.values();
return Response.ok(Map.of("typesMetriques", typesMetriques, "total", typesMetriques.length))
.build();
}
/** Obtient les périodes d'analyse disponibles */
@@ -321,25 +306,9 @@ public class AnalyticsResource {
description = "Récupère la liste de toutes les périodes d'analyse disponibles")
@APIResponse(responseCode = "200", description = "Périodes d'analyse récupérées avec succès")
public Response obtenirPeriodesAnalyse() {
try {
log.info("Récupération des périodes d'analyse disponibles");
PeriodeAnalyse[] periodesAnalyse = PeriodeAnalyse.values();
return Response.ok(
Map.of("periodesAnalyse", periodesAnalyse, "total", periodesAnalyse.length))
.build();
} catch (Exception e) {
log.error("Erreur lors de la récupération des périodes d'analyse: {}", e.getMessage(), e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(
Map.of(
"error",
"Erreur lors de la récupération des périodes d'analyse",
"message",
e.getMessage()))
.build();
}
log.info("Récupération des périodes d'analyse disponibles");
PeriodeAnalyse[] periodesAnalyse = PeriodeAnalyse.values();
return Response.ok(Map.of("periodesAnalyse", periodesAnalyse, "total", periodesAnalyse.length))
.build();
}
}

View File

@@ -47,7 +47,8 @@ public class ApprovalResource {
try {
String transactionId = (String) request.get("transactionId");
String transactionType = (String) request.get("transactionType");
Double amount = ((Number) request.get("amount")).doubleValue();
Number rawAmount = (Number) request.get("amount");
Double amount = rawAmount != null ? rawAmount.doubleValue() : null;
String organizationIdStr = (String) request.get("organizationId");
if (transactionId == null || transactionType == null || amount == null) {

View File

@@ -101,13 +101,8 @@ public class EvenementResource {
// Convertir en DTO mobile
List<EvenementMobileDTO> evenementsDTOs = new ArrayList<>();
for (Evenement evenement : evenements) {
try {
EvenementMobileDTO dto = EvenementMobileDTO.fromEntity(evenement);
evenementsDTOs.add(dto);
} catch (Exception e) {
LOG.errorf("Erreur lors de la conversion de l'événement %s: %s", evenement.getId(), e.getMessage());
// Continuer avec les autres événements
}
EvenementMobileDTO dto = EvenementMobileDTO.fromEntity(evenement);
evenementsDTOs.add(dto);
}
LOG.infof("Nombre de DTOs créés: %d", evenementsDTOs.size());

View File

@@ -40,27 +40,19 @@ public class FinanceWorkflowResource {
@QueryParam("endDate") String endDate) {
LOG.infof("GET /api/finance/stats?organizationId=%s", organizationId);
try {
Map<String, Object> stats = new HashMap<>();
stats.put("totalApprovals", 0);
stats.put("pendingApprovals", 0);
stats.put("approvedCount", 0);
stats.put("rejectedCount", 0);
stats.put("totalBudgets", 0);
stats.put("activeBudgets", 0);
stats.put("averageApprovalTime", "0 hours");
stats.put("period", Map.of(
"startDate", startDate != null ? startDate : LocalDateTime.now().minusMonths(1).toString(),
"endDate", endDate != null ? endDate : LocalDateTime.now().toString()
));
return Response.ok(stats).build();
} catch (Exception e) {
LOG.error("Erreur lors de la récupération des statistiques", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse(e.getMessage()))
.build();
}
Map<String, Object> stats = new HashMap<>();
stats.put("totalApprovals", 0);
stats.put("pendingApprovals", 0);
stats.put("approvedCount", 0);
stats.put("rejectedCount", 0);
stats.put("totalBudgets", 0);
stats.put("activeBudgets", 0);
stats.put("averageApprovalTime", "0 hours");
stats.put("period", Map.of(
"startDate", startDate != null ? startDate : LocalDateTime.now().minusMonths(1).toString(),
"endDate", endDate != null ? endDate : LocalDateTime.now().toString()
));
return Response.ok(stats).build();
}
@GET
@@ -78,15 +70,8 @@ public class FinanceWorkflowResource {
@QueryParam("limit") @DefaultValue("100") int limit) {
LOG.infof("GET /api/finance/audit-logs?organizationId=%s&limit=%d", organizationId, limit);
try {
// Retourne une liste vide pour l'instant - à implémenter plus tard avec vraie persistence
return Response.ok(new ArrayList<>()).build();
} catch (Exception e) {
LOG.error("Erreur lors de la récupération des logs d'audit", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse(e.getMessage()))
.build();
}
// Retourne une liste vide pour l'instant - à implémenter plus tard avec vraie persistence
return Response.ok(new ArrayList<>()).build();
}
@GET
@@ -100,15 +85,8 @@ public class FinanceWorkflowResource {
@QueryParam("endDate") String endDate) {
LOG.infof("GET /api/finance/audit-logs/anomalies?organizationId=%s", organizationId);
try {
// Retourne une liste vide pour l'instant - à implémenter plus tard
return Response.ok(new ArrayList<>()).build();
} catch (Exception e) {
LOG.error("Erreur lors de la récupération des anomalies", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse(e.getMessage()))
.build();
}
// Retourne une liste vide pour l'instant - à implémenter plus tard
return Response.ok(new ArrayList<>()).build();
}
@POST
@@ -122,24 +100,15 @@ public class FinanceWorkflowResource {
LOG.infof("POST /api/finance/audit-logs/export - format: %s", format);
try {
// Pour l'instant, retourne un URL fictif - à implémenter plus tard
String exportUrl = "/api/finance/exports/" + UUID.randomUUID() + "." + format;
// Pour l'instant, retourne un URL fictif - à implémenter plus tard
String exportUrl = "/api/finance/exports/" + UUID.randomUUID() + "." + format;
Map<String, Object> response = new HashMap<>();
response.put("exportUrl", exportUrl);
response.put("format", format);
response.put("status", "generated");
response.put("expiresAt", LocalDateTime.now().plusHours(24).toString());
Map<String, Object> response = new HashMap<>();
response.put("exportUrl", exportUrl);
response.put("format", format);
response.put("status", "generated");
response.put("expiresAt", LocalDateTime.now().plusHours(24).toString());
return Response.ok(response).build();
} catch (Exception e) {
LOG.error("Erreur lors de l'export des logs d'audit", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse(e.getMessage()))
.build();
}
return Response.ok(response).build();
}
record ErrorResponse(String message) {}
}

View File

@@ -85,10 +85,8 @@ public class MembreResource {
: Sort.by(sortField).ascending();
// Filtrage par rôle : ADMIN_ORGANISATION ne voit que ses organisations
java.util.Set<String> roles = securityIdentity.getRoles() != null
? securityIdentity.getRoles()
: java.util.Set.of();
boolean onlyOrgAdmin = roles.contains("ADMIN_ORGANISATION")
java.util.Set<String> roles = securityIdentity.getRoles();
boolean onlyOrgAdmin = roles != null && roles.contains("ADMIN_ORGANISATION")
&& !roles.contains("ADMIN")
&& !roles.contains("SUPER_ADMIN");
@@ -96,9 +94,8 @@ public class MembreResource {
long totalElements;
if (onlyOrgAdmin) {
String email = securityIdentity.getPrincipal() != null
? securityIdentity.getPrincipal().getName()
: null;
java.security.Principal p = securityIdentity.getPrincipal();
String email = p != null ? p.getName() : null;
if (email == null || email.isEmpty()) {
LOG.warn("ADMIN_ORGANISATION sans email identifié - retour liste vide");
@@ -184,10 +181,8 @@ public class MembreResource {
Membre nouveauMembre = membreService.creerMembre(membre);
// Validation périmètre ADMIN_ORGANISATION - lier le membre à l'organisation
java.util.Set<String> roles = securityIdentity.getRoles() != null
? securityIdentity.getRoles()
: java.util.Set.of();
boolean onlyOrgAdmin = roles.contains("ADMIN_ORGANISATION")
java.util.Set<String> roles = securityIdentity.getRoles();
boolean onlyOrgAdmin = roles != null && roles.contains("ADMIN_ORGANISATION")
&& !roles.contains("ADMIN")
&& !roles.contains("SUPER_ADMIN");
@@ -199,9 +194,7 @@ public class MembreResource {
}
// Vérifier que l'utilisateur a accès à cette organisation
String email = securityIdentity.getPrincipal() != null
? securityIdentity.getPrincipal().getName()
: null;
String email = securityIdentity.getPrincipal().getName();
List<UUID> userOrgIds = organisationService.listerOrganisationsPourUtilisateur(email)
.stream()
@@ -545,7 +538,7 @@ public class MembreResource {
}
if (fileName == null || fileName.isEmpty()) {
fileName = file.fileName() != null ? file.fileName() : "import.xlsx";
fileName = file.fileName();
}
if (typeMembreDefaut == null || typeMembreDefaut.isEmpty()) {
@@ -557,10 +550,8 @@ public class MembreResource {
: null;
// Validation périmètre ADMIN_ORGANISATION : doit avoir accès à l'organisation
java.util.Set<String> roles = securityIdentity.getRoles() != null
? securityIdentity.getRoles()
: java.util.Set.of();
boolean onlyOrgAdmin = roles.contains("ADMIN_ORGANISATION")
java.util.Set<String> roles = securityIdentity.getRoles();
boolean onlyOrgAdmin = roles != null && roles.contains("ADMIN_ORGANISATION")
&& !roles.contains("ADMIN")
&& !roles.contains("SUPER_ADMIN");
@@ -571,9 +562,8 @@ public class MembreResource {
.build();
}
String email = securityIdentity.getPrincipal() != null
? securityIdentity.getPrincipal().getName()
: null;
java.security.Principal p2 = securityIdentity.getPrincipal();
String email = p2 != null ? p2.getName() : null;
if (email == null || email.isEmpty()) {
return Response.status(Response.Status.UNAUTHORIZED)

View File

@@ -1,8 +1,10 @@
package dev.lions.unionflow.server.resource;
import dev.lions.unionflow.server.api.dto.common.PagedResponse;
import dev.lions.unionflow.server.api.dto.organisation.request.CreateOrganisationRequest;
import dev.lions.unionflow.server.api.dto.organisation.request.UpdateOrganisationRequest;
import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationResponse;
import dev.lions.unionflow.server.api.dto.organisation.response.OrganisationSummaryResponse;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.service.KeycloakService;
import dev.lions.unionflow.server.service.OrganisationService;
@@ -84,7 +86,7 @@ public class OrganisationResource {
/** Crée une nouvelle organisation */
@POST
@RolesAllowed({"ADMIN", "MEMBRE"})
@RolesAllowed({"SUPER_ADMIN", "ADMIN", "MEMBRE"})
@Operation(
summary = "Créer une nouvelle organisation",
@@ -113,7 +115,7 @@ public class OrganisationResource {
return Response.created(URI.create("/api/organisations/" + organisationCreee.getId()))
.entity(dto)
.build();
} catch (IllegalArgumentException e) {
} catch (IllegalArgumentException | IllegalStateException e) {
LOG.warnf("Erreur lors de la création de l'organisation: %s", e.getMessage());
return Response.status(Response.Status.CONFLICT)
.entity(Map.of("error", e.getMessage()))
@@ -139,11 +141,11 @@ public class OrganisationResource {
content =
@Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(type = SchemaType.ARRAY, implementation = OrganisationResponse.class))),
schema = @Schema(implementation = PagedResponse.class))),
@APIResponse(responseCode = "401", description = "Non authentifié"),
@APIResponse(responseCode = "403", description = "Non autorisé")
})
public Response listerOrganisations(
public PagedResponse<OrganisationSummaryResponse> listerOrganisations(
@Parameter(description = "Numéro de page (commence à 0)", example = "0")
@QueryParam("page")
@DefaultValue("0")
@@ -159,47 +161,44 @@ public class OrganisationResource {
"Récupération des organisations - page: %d, size: %d, recherche: %s",
page, size, recherche);
try {
List<Organisation> organisations;
List<Organisation> organisations;
long totalElements;
// Admin d'organisation (sans rôle ADMIN/SUPER_ADMIN) : ne retourner que ses organisations
java.util.Set<String> roles = securityIdentity.getRoles() != null ? securityIdentity.getRoles() : java.util.Set.of();
boolean onlyOrgAdmin = roles.contains("ADMIN_ORGANISATION")
&& !roles.contains("ADMIN")
&& !roles.contains("SUPER_ADMIN");
if (onlyOrgAdmin && securityIdentity.getPrincipal() != null) {
String email = securityIdentity.getPrincipal().getName();
organisations = organisationService.listerOrganisationsPourUtilisateur(email);
if (recherche != null && !recherche.trim().isEmpty()) {
String term = recherche.trim().toLowerCase();
organisations = organisations.stream()
.filter(o -> (o.getNom() != null && o.getNom().toLowerCase().contains(term))
|| (o.getNomCourt() != null && o.getNomCourt().toLowerCase().contains(term)))
.collect(Collectors.toList());
}
// Pagination en mémoire pour /mes
int from = Math.min(page * size, organisations.size());
int to = Math.min(from + size, organisations.size());
organisations = organisations.subList(from, to);
LOG.infof("ADMIN_ORGANISATION: retour de %d organisation(s) pour %s", organisations.size(), email);
} else if (recherche != null && !recherche.trim().isEmpty()) {
organisations = organisationService.rechercherOrganisations(recherche.trim(), page, size);
} else {
organisations = organisationService.listerOrganisationsActives(page, size);
// Admin d'organisation (sans rôle ADMIN/SUPER_ADMIN) : ne retourner que ses organisations
java.util.Set<String> roles = securityIdentity.getRoles();
boolean onlyOrgAdmin = roles.contains("ADMIN_ORGANISATION")
&& !roles.contains("ADMIN")
&& !roles.contains("SUPER_ADMIN");
if (onlyOrgAdmin && securityIdentity.getPrincipal() != null) {
String email = securityIdentity.getPrincipal().getName();
organisations = organisationService.listerOrganisationsPourUtilisateur(email);
if (recherche != null && !recherche.trim().isEmpty()) {
String term = recherche.trim().toLowerCase();
organisations = organisations.stream()
.filter(o -> (o.getNom() != null && o.getNom().toLowerCase().contains(term))
|| (o.getNomCourt() != null && o.getNomCourt().toLowerCase().contains(term)))
.collect(Collectors.toList());
}
List<OrganisationResponse> dtos =
organisations.stream()
.map(organisationService::convertToResponse)
.collect(Collectors.toList());
return Response.ok(dtos).build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la récupération des organisations");
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(Map.of("error", "Erreur interne du serveur"))
.build();
totalElements = organisations.size();
// Pagination en mémoire pour /mes
int from = Math.min(page * size, organisations.size());
int to = Math.min(from + size, organisations.size());
organisations = organisations.subList(from, to);
LOG.infof("ADMIN_ORGANISATION: retour de %d organisation(s) pour %s", organisations.size(), email);
} else if (recherche != null && !recherche.trim().isEmpty()) {
organisations = organisationService.rechercherOrganisations(recherche.trim(), page, size);
totalElements = organisationService.rechercherOrganisationsCount(recherche.trim());
} else {
organisations = organisationService.listerOrganisationsActives(page, size);
totalElements = organisationService.compterOrganisationsActives();
}
List<OrganisationSummaryResponse> dtos =
organisations.stream()
.map(organisationService::convertToSummaryResponse)
.collect(Collectors.toList());
return new PagedResponse<>(dtos, totalElements, page, size);
}
/** Récupère une organisation par son ID */
@@ -235,7 +234,9 @@ public class OrganisationResource {
})
.orElse(
Response.status(Response.Status.NOT_FOUND)
.entity(Map.of("error", "Organisation non trouvée"))
.entity(Map.of(
"error", "Organisation non trouvée",
"status", 404))
.build());
}
@@ -309,7 +310,7 @@ public class OrganisationResource {
public Response supprimerOrganisation(
@Parameter(description = "UUID de l'organisation", required = true) @PathParam("id") UUID id) {
LOG.infof("Suppression de l'organisation ID: %d", id);
LOG.infof("Suppression de l'organisation ID: %s", id);
try {
organisationService.supprimerOrganisation(id, "system");
@@ -398,7 +399,7 @@ public class OrganisationResource {
public Response activerOrganisation(
@Parameter(description = "UUID de l'organisation", required = true) @PathParam("id") UUID id) {
LOG.infof("Activation de l'organisation ID: %d", id);
LOG.infof("Activation de l'organisation ID: %s", id);
try {
Organisation organisation = organisationService.activerOrganisation(id, "system");
@@ -433,7 +434,7 @@ public class OrganisationResource {
public Response suspendreOrganisation(
@Parameter(description = "UUID de l'organisation", required = true) @PathParam("id") UUID id) {
LOG.infof("Suspension de l'organisation ID: %d", id);
LOG.infof("Suspension de l'organisation ID: %s", id);
try {
Organisation organisation = organisationService.suspendreOrganisation(id, "system");

View File

@@ -210,10 +210,7 @@ public class AdhesionService {
"Seules les adhésions approuvées peuvent receive un paiement");
}
BigDecimal nouveauMontant = adhesion.getMontantPaye() != null
? adhesion.getMontantPaye().add(montantPaye)
: montantPaye;
adhesion.setMontantPaye(nouveauMontant);
adhesion.setMontantPaye(adhesion.getMontantPaye().add(montantPaye));
if (adhesion.isPayeeIntegralement()) {
adhesion.setStatut("APPROUVEE");
@@ -283,9 +280,6 @@ public class AdhesionService {
}
private AdhesionResponse convertToDTO(DemandeAdhesion adhesion) {
if (adhesion == null)
return null;
AdhesionResponse response = new AdhesionResponse();
response.setId(adhesion.getId());
response.setNumeroReference(adhesion.getNumeroReference());
@@ -328,21 +322,12 @@ public class AdhesionService {
}
private DemandeAdhesion convertToEntity(CreateAdhesionRequest request) {
if (request == null)
return null;
return DemandeAdhesion.builder()
.numeroReference(
request.numeroReference() != null
? request.numeroReference()
: DemandeAdhesion.genererNumeroReference())
.dateDemande(
request.dateDemande() != null
? request.dateDemande().atStartOfDay()
: LocalDateTime.now())
.fraisAdhesion(request.fraisAdhesion() != null ? request.fraisAdhesion() : BigDecimal.ZERO)
.numeroReference(request.numeroReference())
.dateDemande(request.dateDemande().atStartOfDay())
.fraisAdhesion(request.fraisAdhesion())
.montantPaye(BigDecimal.ZERO)
.codeDevise(request.codeDevise() != null ? request.codeDevise() : defaultsService.getDevise())
.codeDevise(request.codeDevise())
.statut("EN_ATTENTE")
.observations(request.observations())
.build();

View File

@@ -101,7 +101,7 @@ public class AdminUserService {
List<String> currentNames = getUserRoles(userId).stream()
.map(RoleDTO::getName)
.collect(Collectors.toList());
List<String> toAssign = targetRoleNames == null ? List.of() : new ArrayList<>(targetRoleNames);
List<String> toAssign = new ArrayList<>(targetRoleNames != null ? targetRoleNames : List.of());
toAssign.removeAll(currentNames);
List<String> toRevoke = new ArrayList<>(currentNames);
toRevoke.removeAll(targetRoleNames == null ? List.of() : targetRoleNames);

View File

@@ -83,10 +83,8 @@ public class AdresseService {
.findAdresseById(id)
.orElseThrow(() -> new NotFoundException("Adresse non trouvée avec l'ID: " + id));
// Mise à jour des champs
updateFromDTO(adresse, request);
// Gestion de l'adresse principale
// Gestion de l'adresse principale AVANT la mise à jour pour éviter l'auto-flush Hibernate
// qui inclurait l'entité courante dans la requête JPQL si elle est déjà dirty
if (Boolean.TRUE.equals(request.principale())) {
desactiverAutresPrincipales(
adresse.getOrganisation() != null ? adresse.getOrganisation().getId() : null,
@@ -94,6 +92,9 @@ public class AdresseService {
adresse.getEvenement() != null ? adresse.getEvenement().getId() : null);
}
// Mise à jour des champs
updateFromDTO(adresse, request);
adresseRepository.persist(adresse);
LOG.infof("Adresse mise à jour avec succès: ID=%s", id);
@@ -281,7 +282,7 @@ public class AdresseService {
adresse.setPays(request.pays());
adresse.setLatitude(request.latitude());
adresse.setLongitude(request.longitude());
adresse.setPrincipale(request.principale() != null ? request.principale() : false);
adresse.setPrincipale(Boolean.TRUE.equals(request.principale()));
adresse.setLibelle(request.libelle());
adresse.setNotes(request.notes());

View File

@@ -12,6 +12,7 @@ import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.UUID;
@@ -73,7 +74,7 @@ public class AlerteLcbFtService {
if (montant != null && seuil != null) {
BigDecimal ecart = montant.subtract(seuil);
BigDecimal ratio = seuil.compareTo(BigDecimal.ZERO) > 0 ?
ecart.divide(seuil, 2, BigDecimal.ROUND_HALF_UP) : BigDecimal.ZERO;
ecart.divide(seuil, 2, RoundingMode.HALF_UP) : BigDecimal.ZERO;
if (ratio.compareTo(new BigDecimal("2.0")) >= 0) { // > 200% du seuil
severite = "CRITICAL";

View File

@@ -151,10 +151,19 @@ public class AuditService {
query.where(predicates.toArray(new jakarta.persistence.criteria.Predicate[0]));
query.orderBy(cb.desc(root.get("dateHeure")));
// Compter le total
// Compter le total (avec son propre root pour éviter le partage de prédicats)
var countQuery = cb.createQuery(Long.class);
countQuery.select(cb.count(countQuery.from(AuditLog.class)));
countQuery.where(predicates.toArray(new jakarta.persistence.criteria.Predicate[0]));
var countRoot = countQuery.from(AuditLog.class);
countQuery.select(cb.count(countRoot));
var countPredicates = new ArrayList<jakarta.persistence.criteria.Predicate>();
if (dateDebut != null) countPredicates.add(cb.greaterThanOrEqualTo(countRoot.get("dateHeure"), dateDebut));
if (dateFin != null) countPredicates.add(cb.lessThanOrEqualTo(countRoot.get("dateHeure"), dateFin));
if (typeAction != null && !typeAction.isEmpty()) countPredicates.add(cb.equal(countRoot.get("typeAction"), typeAction));
if (severite != null && !severite.isEmpty()) countPredicates.add(cb.equal(countRoot.get("severite"), severite));
if (utilisateur != null && !utilisateur.isEmpty()) countPredicates.add(cb.like(cb.lower(countRoot.get("utilisateur")), "%" + utilisateur.toLowerCase() + "%"));
if (module != null && !module.isEmpty()) countPredicates.add(cb.equal(countRoot.get("module"), module));
if (ipAddress != null && !ipAddress.isEmpty()) countPredicates.add(cb.like(countRoot.get("ipAddress"), "%" + ipAddress + "%"));
countQuery.where(countPredicates.toArray(new jakarta.persistence.criteria.Predicate[0]));
long total = entityManager.createQuery(countQuery).getSingleResult();
// Récupérer les résultats avec pagination

View File

@@ -424,7 +424,7 @@ public class CotisationService {
response.setDatePaiementFormattee(cotisation.getDatePaiement() != null
? cotisation.getDatePaiement().format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm", Locale.FRANCE))
: null);
if (cotisation.isEnRetard() && cotisation.getDateEcheance() != null) {
if (cotisation.isEnRetard()) {
long jours = java.time.temporal.ChronoUnit.DAYS.between(cotisation.getDateEcheance(), LocalDate.now());
response.setRetardCouleur("text-red-500");
response.setRetardTexte(jours + " jour" + (jours > 1 ? "s" : "") + " de retard");

View File

@@ -82,12 +82,12 @@ public class DashboardServiceImpl implements DashboardService {
// Gérer le cas où organizationId est vide ou invalide
UUID orgId = parseOrganizationId(organizationId);
java.util.Set<UUID> orgIds = orgId != null ? java.util.Set.of(orgId) : null;
java.util.Set<UUID> orgIds = orgId != null ? java.util.Set.of(orgId) : java.util.Set.of();
// Compter les membres (par organisation si orgId fourni, sinon global)
long totalMembers;
long activeMembers;
if (orgIds != null && !orgIds.isEmpty()) {
if (!orgIds.isEmpty()) {
totalMembers = membreRepository.countDistinctByOrganisationIdIn(orgIds);
activeMembers = membreRepository.countActifsDistinctByOrganisationIdIn(orgIds);
} else {
@@ -215,7 +215,7 @@ public class DashboardServiceImpl implements DashboardService {
.type("event")
.title("Événement créé")
.description(evenement.getTitre() + " a été programmé")
.userName(evenement.getOrganisation() != null ? evenement.getOrganisation().getNom() : "Système")
.userName(evenement.getOrganisation().getNom())
.timestamp(evenement.getDateCreation())
.userAvatar(null)
.actionUrl("/events/" + evenement.getId())

View File

@@ -87,8 +87,7 @@ public class DocumentService {
.findDocumentById(id)
.orElseThrow(() -> new NotFoundException("Document non trouvé avec l'ID: " + id));
document.setNombreTelechargements(
(document.getNombreTelechargements() != null ? document.getNombreTelechargements() : 0) + 1);
document.setNombreTelechargements(document.getNombreTelechargements() + 1);
document.setDateDernierTelechargement(LocalDateTime.now());
document.setModifiePar(keycloakService.getCurrentUserEmail());

View File

@@ -53,7 +53,7 @@ public class ExportService {
nomMembre,
c.getTypeCotisation() != null ? c.getTypeCotisation() : "",
c.getMontantDu() != null ? c.getMontantDu().toString() : "0",
c.getMontantPaye() != null ? c.getMontantPaye().toString() : "0",
c.getMontantPaye().toString(),
c.getStatut() != null ? c.getStatut() : "",
c.getDateEcheance() != null ? c.getDateEcheance().format(DATE_FORMATTER) : "",
c.getDatePaiement() != null ? c.getDatePaiement().format(DATETIME_FORMATTER) : "",

View File

@@ -55,7 +55,7 @@ public class FileStorageService {
* @throws IllegalArgumentException Si validation échoue
*/
public FileMetadata storeFile(InputStream inputStream, String fileName, String mimeType, long fileSize)
throws IOException {
throws IOException, java.security.NoSuchAlgorithmException {
// Validation de la taille
if (fileSize > MAX_FILE_SIZE) {
@@ -82,14 +82,10 @@ public class FileStorageService {
Files.createDirectories(targetPath.getParent());
// Écrire le fichier et calculer les hash simultanément
MessageDigest md5 = null;
MessageDigest sha256 = null;
try {
md5 = MessageDigest.getInstance("MD5");
sha256 = MessageDigest.getInstance("SHA-256");
} catch (Exception e) {
log.warn("Impossible de créer les MessageDigest pour hash: {}", e.getMessage());
}
// MD5 et SHA-256 sont des algorithmes obligatoires depuis Java 1.4 (JCA standard)
// et ne peuvent jamais lever NoSuchAlgorithmException dans un JRE conforme.
MessageDigest md5 = MessageDigest.getInstance("MD5");
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
byte[] buffer = new byte[8192];
int bytesRead;
@@ -99,20 +95,14 @@ public class FileStorageService {
while ((bytesRead = inputStream.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
totalBytes += bytesRead;
// Calculer les hash
if (md5 != null) {
md5.update(buffer, 0, bytesRead);
}
if (sha256 != null) {
sha256.update(buffer, 0, bytesRead);
}
md5.update(buffer, 0, bytesRead);
sha256.update(buffer, 0, bytesRead);
}
}
// Générer les hash
String hashMd5 = md5 != null ? bytesToHex(md5.digest()) : null;
String hashSha256 = sha256 != null ? bytesToHex(sha256.digest()) : null;
String hashMd5 = bytesToHex(md5.digest());
String hashSha256 = bytesToHex(sha256.digest());
log.info("Fichier stocké : {} ({} octets) - MD5: {}", uniqueFileName, totalBytes, hashMd5);
@@ -183,6 +173,8 @@ public class FileStorageService {
*/
@lombok.Data
@lombok.Builder
@lombok.NoArgsConstructor
@lombok.AllArgsConstructor
public static class FileMetadata {
private String nomFichier;
private String nomOriginal;

View File

@@ -283,8 +283,7 @@ public class KPICalculatorService {
private BigDecimal calculerKPIMontantAides(
UUID organisationId, LocalDateTime dateDebut, LocalDateTime dateFin) {
BigDecimal total = demandeAideRepository.sumMontantsAccordes(organisationId, dateDebut, dateFin);
return total != null ? total : BigDecimal.ZERO;
return demandeAideRepository.sumMontantsAccordes(organisationId, dateDebut, dateFin);
}
private BigDecimal calculerKPITauxApprobationAides(

View File

@@ -194,7 +194,8 @@ public class KeycloakService {
}
try {
return jwt.getClaimNames();
Set<String> names = jwt.getClaimNames();
return names != null ? names : Set.of();
} catch (Exception e) {
LOG.warnf("Erreur lors de la récupération des claims: %s", e.getMessage());
return Set.of();

View File

@@ -316,7 +316,7 @@ public class MatchingService {
// 5. Disponibilité et capacité (10 points max)
if (proposition.peutAccepterBeneficiaires()) {
double ratioCapacite = (double) proposition.getPlacesRestantes()
/ (proposition.getNombreMaxBeneficiaires() != null ? proposition.getNombreMaxBeneficiaires() : 1);
/ proposition.getNombreMaxBeneficiaires();
score += 10.0 * ratioCapacite;
}

View File

@@ -126,7 +126,7 @@ public class MembreDashboardService {
.filter(d -> d.getStatut() != null && d.getStatut() != StatutAide.APPROUVEE && d.getStatut() != StatutAide.REJETEE && d.getStatut() != StatutAide.ANNULEE)
.count() : 0;
Integer tauxAidesApprouvees = null;
if (mesDemandes > 0 && demandes != null) {
if (mesDemandes > 0) {
long acceptees = demandes.stream().filter(d -> d.getStatut() == StatutAide.APPROUVEE).count();
tauxAidesApprouvees = (int) (acceptees * 100 / mesDemandes);
}

View File

@@ -37,6 +37,7 @@ import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
import java.time.format.DateTimeParseException;
import java.util.*;
@@ -99,7 +100,7 @@ public class MembreImportExportService {
"Format de fichier non supporté. Formats acceptés: .xlsx, .xls, .csv");
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de l'import");
resultat.erreurs.add(e.getMessage() != null ? e.getMessage() : "Erreur inconnue lors de l'import");
resultat.erreurs.add(Objects.toString(e.getMessage(), "Erreur inconnue lors de l'import"));
return resultat;
}
}
@@ -182,9 +183,9 @@ public class MembreImportExportService {
} else {
if (souscriptionOpt.isPresent()) {
SouscriptionOrganisation souscription = souscriptionOpt.get();
if (souscription.isQuotaDepasse() || souscription.getPlacesRestantes() <= 0) {
String msg = String.format("Ligne %d: Quota souscription atteint (max %d membres).",
ligneNum, souscription.getQuotaMax() != null ? souscription.getQuotaMax() : "?");
if (souscription.isQuotaDepasse()) {
String msg = String.format("Ligne %d: Quota souscription atteint (max %s membres).",
ligneNum, souscription.getQuotaMax());
resultat.erreurs.add(msg);
if (!ignorerErreurs) throw new IllegalArgumentException(msg);
continue;
@@ -273,9 +274,9 @@ public class MembreImportExportService {
} else {
if (souscriptionOpt.isPresent()) {
SouscriptionOrganisation souscription = souscriptionOpt.get();
if (souscription.isQuotaDepasse() || souscription.getPlacesRestantes() <= 0) {
String msg = String.format("Ligne %d: Quota souscription atteint (max %d membres).",
ligneNum, souscription.getQuotaMax() != null ? souscription.getQuotaMax() : "?");
if (souscription.isQuotaDepasse()) {
String msg = String.format("Ligne %d: Quota souscription atteint (max %s membres).",
ligneNum, souscription.getQuotaMax());
resultat.erreurs.add(msg);
if (!ignorerErreurs) throw new IllegalArgumentException(msg);
continue;
@@ -791,13 +792,9 @@ public class MembreImportExportService {
protection.setLockStructure(true);
// Le mot de passe doit être haché selon le format Excel
// Pour simplifier, on utilise le hash MD5 du mot de passe
try {
java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5");
byte[] passwordHash = md.digest(motDePasse.getBytes(java.nio.charset.StandardCharsets.UTF_16LE));
protection.setWorkbookPassword(passwordHash);
} catch (java.security.NoSuchAlgorithmException e) {
LOG.warnf("Impossible de hasher le mot de passe, protection partielle uniquement");
}
java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5");
byte[] passwordHash = md.digest(motDePasse.getBytes(java.nio.charset.StandardCharsets.UTF_16LE));
protection.setWorkbookPassword(passwordHash);
workbook.write(outputStream);
return outputStream.toByteArray();
@@ -865,7 +862,7 @@ public class MembreImportExportService {
values.add(membre.getStatutCompte() != null ? membre.getStatutCompte() : "");
}
if (colonnesExport.contains("ORGANISATION") || colonnesExport.isEmpty()) {
values.add(membre.getAssociationNom() != null ? membre.getAssociationNom() : "");
values.add(Objects.toString(membre.getAssociationNom(), ""));
}
printer.printRecord(values);
@@ -981,9 +978,6 @@ public class MembreImportExportService {
document.close();
return outputStream.toByteArray();
} catch (DocumentException e) {
LOG.errorf(e, "Erreur lors de la génération du PDF");
throw new IOException("Erreur lors de la génération du PDF", e);
}
}

View File

@@ -214,7 +214,7 @@ public class MembreService {
* Sinon retourne Optional.empty() pour indiquer "tous les membres".
*/
private Optional<Set<UUID>> getOrganisationIdsForCurrentUserIfAdminOrg() {
if (securityIdentity == null || securityIdentity.getPrincipal() == null) return Optional.empty();
if (securityIdentity.getPrincipal() == null) return Optional.empty();
Set<String> roles = securityIdentity.getRoles();
if (roles == null) return Optional.empty();
boolean adminOrg = roles.contains("ADMIN_ORGANISATION");
@@ -490,71 +490,65 @@ public class MembreService {
}
}
try {
// Construction de la requête dynamique
StringBuilder queryBuilder = new StringBuilder("SELECT m FROM Membre m WHERE 1=1");
Map<String, Object> parameters = new HashMap<>();
// Construction de la requête dynamique
StringBuilder queryBuilder = new StringBuilder("SELECT m FROM Membre m WHERE 1=1");
Map<String, Object> parameters = new HashMap<>();
// Ajout des critères de recherche
addSearchCriteria(queryBuilder, parameters, criteria);
// Ajout des critères de recherche
addSearchCriteria(queryBuilder, parameters, criteria);
// Requête pour compter le total
String countQuery = queryBuilder
.toString()
.replace("SELECT m FROM Membre m", "SELECT COUNT(m) FROM Membre m");
// Requête pour compter le total
String countQuery = queryBuilder
.toString()
.replace("SELECT m FROM Membre m", "SELECT COUNT(m) FROM Membre m");
// Exécution de la requête de comptage
TypedQuery<Long> countQueryTyped = entityManager.createQuery(countQuery, Long.class);
for (Map.Entry<String, Object> param : parameters.entrySet()) {
countQueryTyped.setParameter(param.getKey(), param.getValue());
}
long totalElements = countQueryTyped.getSingleResult();
if (totalElements == 0) {
return MembreSearchResultDTO.empty(criteria, page.size, page.index);
}
// Ajout du tri et pagination
String finalQuery = queryBuilder.toString();
if (sort != null) {
finalQuery += " ORDER BY " + buildOrderByClause(sort);
}
// Exécution de la requête principale
TypedQuery<Membre> queryTyped = entityManager.createQuery(finalQuery, Membre.class);
for (Map.Entry<String, Object> param : parameters.entrySet()) {
queryTyped.setParameter(param.getKey(), param.getValue());
}
queryTyped.setFirstResult(page.index * page.size);
queryTyped.setMaxResults(page.size);
List<Membre> membres = queryTyped.getResultList();
// Conversion en SummaryResponses
List<MembreSummaryResponse> membresDTO = convertToSummaryResponseList(membres);
// Calcul des statistiques
MembreSearchResultDTO.SearchStatistics statistics = calculateSearchStatistics(membres);
// Construction du résultat
MembreSearchResultDTO result = MembreSearchResultDTO.builder()
.membres(membresDTO)
.totalElements(totalElements)
.totalPages((int) Math.ceil((double) totalElements / page.size))
.currentPage(page.index)
.pageSize(page.size)
.criteria(criteria)
.statistics(statistics)
.build();
// Calcul des indicateurs de pagination
result.calculatePaginationFlags();
return result;
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de la recherche avancée de membres");
throw new RuntimeException("Erreur lors de la recherche avancée", e);
// Exécution de la requête de comptage
TypedQuery<Long> countQueryTyped = entityManager.createQuery(countQuery, Long.class);
for (Map.Entry<String, Object> param : parameters.entrySet()) {
countQueryTyped.setParameter(param.getKey(), param.getValue());
}
long totalElements = countQueryTyped.getSingleResult();
if (totalElements == 0) {
return MembreSearchResultDTO.empty(criteria, page.size, page.index);
}
// Ajout du tri et pagination
String finalQuery = queryBuilder.toString();
if (sort != null) {
finalQuery += " ORDER BY " + buildOrderByClause(sort);
}
// Exécution de la requête principale
TypedQuery<Membre> queryTyped = entityManager.createQuery(finalQuery, Membre.class);
for (Map.Entry<String, Object> param : parameters.entrySet()) {
queryTyped.setParameter(param.getKey(), param.getValue());
}
queryTyped.setFirstResult(page.index * page.size);
queryTyped.setMaxResults(page.size);
List<Membre> membres = queryTyped.getResultList();
// Conversion en SummaryResponses
List<MembreSummaryResponse> membresDTO = convertToSummaryResponseList(membres);
// Calcul des statistiques
MembreSearchResultDTO.SearchStatistics statistics = calculateSearchStatistics(membres);
// Construction du résultat
MembreSearchResultDTO result = MembreSearchResultDTO.builder()
.membres(membresDTO)
.totalElements(totalElements)
.totalPages((int) Math.ceil((double) totalElements / page.size))
.currentPage(page.index)
.pageSize(page.size)
.criteria(criteria)
.statistics(statistics)
.build();
// Calcul des indicateurs de pagination
result.calculatePaginationFlags();
return result;
}
/** Ajoute les critères de recherche à la requête */
@@ -649,7 +643,7 @@ public class MembreService {
/** Construit la clause ORDER BY à partir du Sort */
private String buildOrderByClause(Sort sort) {
if (sort == null || sort.getColumns().isEmpty()) {
if (sort.getColumns().isEmpty()) {
return "m.nom ASC";
}
@@ -776,12 +770,12 @@ public class MembreService {
* @return Données binaires du fichier Excel
*/
public byte[] exporterMembresSelectionnes(List<UUID> membreIds, String format) {
LOG.infof("Export de %d membres sélectionnés - format: %s", membreIds.size(), format);
if (membreIds == null || membreIds.isEmpty()) {
throw new IllegalArgumentException("La liste des membres ne peut pas être vide");
}
LOG.infof("Export de %d membres sélectionnés - format: %s", membreIds.size(), format);
// Récupérer les membres
List<Membre> membres = membreIds.stream()
.map(id -> membreRepository.findByIdOptional(id))

View File

@@ -189,13 +189,13 @@ public class NotificationService {
@Transactional
public int envoyerNotificationsGroupees(
List<UUID> membreIds, String sujet, String corps, List<String> canaux) {
LOG.infof(
"Envoi de notifications groupées à %d membres - sujet: %s", membreIds.size(), sujet);
if (membreIds == null || membreIds.isEmpty()) {
throw new IllegalArgumentException("La liste des membres ne peut pas être vide");
}
LOG.infof(
"Envoi de notifications groupées à %d membres - sujet: %s", membreIds.size(), sujet);
int notificationsCreees = 0;
for (UUID membreId : membreIds) {
try {

View File

@@ -356,6 +356,15 @@ public class OrganisationService {
return organisationRepository.findAllActives(Page.of(page, size), Sort.by("nom").ascending());
}
/**
* Compte le nombre d'organisations actives
*
* @return nombre total d'organisations actives
*/
public long compterOrganisationsActives() {
return organisationRepository.countActives();
}
/**
* Recherche d'organisations par nom
*
@@ -559,14 +568,11 @@ public class OrganisationService {
dto.setCreePar(organisation.getCreePar());
dto.setModifiePar(organisation.getModifiePar());
dto.setActif(organisation.getActif());
dto.setVersion(organisation.getVersion() != null ? organisation.getVersion() : 0L);
dto.setVersion(organisation.getVersion());
dto.setOrganisationPublique(
organisation.getOrganisationPublique() != null ? organisation.getOrganisationPublique() : true);
dto.setAccepteNouveauxMembres(
organisation.getAccepteNouveauxMembres() != null ? organisation.getAccepteNouveauxMembres() : true);
dto.setCotisationObligatoire(
organisation.getCotisationObligatoire() != null ? organisation.getCotisationObligatoire() : false);
dto.setOrganisationPublique(organisation.getOrganisationPublique());
dto.setAccepteNouveauxMembres(organisation.getAccepteNouveauxMembres());
dto.setCotisationObligatoire(organisation.getCotisationObligatoire());
dto.setMontantCotisationAnnuelle(organisation.getMontantCotisationAnnuelle());
return dto;

View File

@@ -545,10 +545,6 @@ public class PaiementService {
/** Convertit une entité en Response DTO */
private PaiementResponse convertToResponse(Paiement paiement) {
if (paiement == null) {
return null;
}
PaiementResponse response = new PaiementResponse();
response.setId(paiement.getId());
response.setNumeroReference(paiement.getNumeroReference());

View File

@@ -108,9 +108,9 @@ public class PropositionAideService {
if (response == null) {
// Si non trouvé dans le cache, essayer de simuler depuis BDD
response = simulerRecuperationBDD(id);
if (response == null) {
throw new IllegalArgumentException("Proposition non trouvée: " + id);
}
}
if (response == null) {
throw new IllegalArgumentException("Proposition non trouvée: " + id);
}
if (request.titre() != null)
@@ -162,14 +162,8 @@ public class PropositionAideService {
}
// Simulation de récupération depuis la base de données
response = simulerRecuperationBDD(id);
if (response != null) {
ajouterAuCache(response);
ajouterAIndex(response);
}
return response;
// simulerRecuperationBDD retourne toujours null (stub — à remplacer par un vrai repository)
return simulerRecuperationBDD(id);
}
/**
@@ -253,7 +247,6 @@ public class PropositionAideService {
p.getDonneesPersonnalisees().put("scoreCompatibilite", score);
return p;
})
.filter(p -> (Double) p.getDonneesPersonnalisees().get("scoreCompatibilite") >= 30.0)
.sorted(
(p1, p2) -> {
Double score1 = (Double) p1.getDonneesPersonnalisees().get("scoreCompatibilite");
@@ -430,7 +423,7 @@ public class PropositionAideService {
return false;
}
case "organisationId" -> {
if (!proposition.getOrganisationId().equals(valeur))
if (!java.util.Objects.equals(proposition.getOrganisationId(), valeur))
return false;
}
case "estDisponible" -> {

View File

@@ -245,7 +245,6 @@ public class SystemMetricsService {
File root = new File("/");
long total = root.getTotalSpace();
long free = root.getFreeSpace();
if (total == 0) return 0.0;
return ((total - free) * 100.0) / total;
}
@@ -304,7 +303,15 @@ public class SystemMetricsService {
*/
private Integer getDbConnectionPoolSize() {
if (dataSource instanceof AgroalDataSource agroalDataSource) {
return agroalDataSource.getConfiguration().connectionPoolConfiguration().maxSize();
try {
var config = agroalDataSource.getConfiguration();
if (config == null) return 0;
var poolConfig = config.connectionPoolConfiguration();
if (poolConfig == null) return 0;
return poolConfig.maxSize();
} catch (Exception e) {
return 0;
}
}
return 0;
}
@@ -314,7 +321,13 @@ public class SystemMetricsService {
*/
private Integer getDbActiveConnections() {
if (dataSource instanceof AgroalDataSource agroalDataSource) {
return (int) agroalDataSource.getMetrics().activeCount();
try {
var metrics = agroalDataSource.getMetrics();
if (metrics == null) return 0;
return (int) metrics.activeCount();
} catch (Exception e) {
return 0;
}
}
return 0;
}
@@ -324,7 +337,13 @@ public class SystemMetricsService {
*/
private Integer getDbIdleConnections() {
if (dataSource instanceof AgroalDataSource agroalDataSource) {
return (int) agroalDataSource.getMetrics().availableCount();
try {
var metrics = agroalDataSource.getMetrics();
if (metrics == null) return 0;
return (int) metrics.availableCount();
} catch (Exception e) {
return 0;
}
}
return 0;
}

View File

@@ -208,10 +208,7 @@ public class TrendAnalysisService {
BigDecimal numerateur = nBD.multiply(sommeXY).subtract(sommeX.multiply(sommeY));
BigDecimal denominateur = nBD.multiply(sommeX2).subtract(sommeX.multiply(sommeX));
BigDecimal pente =
denominateur.compareTo(BigDecimal.ZERO) != 0
? numerateur.divide(denominateur, 6, RoundingMode.HALF_UP)
: BigDecimal.ZERO;
BigDecimal pente = numerateur.divide(denominateur, 6, RoundingMode.HALF_UP);
// Calcul du coefficient de corrélation R²
BigDecimal numerateurR = numerateur;
@@ -219,15 +216,11 @@ public class TrendAnalysisService {
BigDecimal denominateurR2 = nBD.multiply(sommeY2).subtract(sommeY.multiply(sommeY));
BigDecimal coefficientCorrelation = BigDecimal.ZERO;
if (denominateurR1.compareTo(BigDecimal.ZERO) != 0
&& denominateurR2.compareTo(BigDecimal.ZERO) != 0) {
BigDecimal denominateurR =
new BigDecimal(Math.sqrt(denominateurR1.multiply(denominateurR2).doubleValue()));
if (denominateurR.compareTo(BigDecimal.ZERO) != 0) {
BigDecimal r = numerateurR.divide(denominateurR, 6, RoundingMode.HALF_UP);
coefficientCorrelation = r.multiply(r); // R²
}
BigDecimal produit = denominateurR1.multiply(denominateurR2);
if (produit.compareTo(BigDecimal.ZERO) > 0) {
BigDecimal denominateurR = new BigDecimal(Math.sqrt(produit.doubleValue()));
BigDecimal r = numerateurR.divide(denominateurR, 6, RoundingMode.HALF_UP);
coefficientCorrelation = r.multiply(r); // R²
}
return new TendanceDTO(pente, coefficientCorrelation);

View File

@@ -383,7 +383,7 @@ public class DemandeCreditService {
.capitalAmorti(principal)
.interetsDeLaPeriode(interets)
.montantTotalExigible(mensualite)
.capitalRestantDu(capitalRestant.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : capitalRestant)
.capitalRestantDu(capitalRestant.max(BigDecimal.ZERO))
.statut(StatutEcheanceCredit.A_VENIR)
.build();

View File

@@ -70,11 +70,8 @@ public class CompteEpargneService {
compte.setMembre(membre);
compte.setOrganisation(organisation);
// Par défaut, le compte est actif et ouvert aujourd'hui
// Par défaut, le compte est actif (dateOuverture déjà initialisée dans l'entité)
compte.setStatut(StatutCompteEpargne.ACTIF);
if (compte.getDateOuverture() == null) {
compte.setDateOuverture(LocalDate.now());
}
// Générer un numéro de compte s'il n'est pas fourni (il n'est pas dans le DTO
// de requête actuel)
@@ -109,7 +106,8 @@ public class CompteEpargneService {
* @return La liste des comptes visibles pour l'utilisateur connecté.
*/
public List<CompteEpargneResponse> getMesComptes() {
String email = securityIdentity.getPrincipal() != null ? securityIdentity.getPrincipal().getName() : null;
java.security.Principal principal = securityIdentity.getPrincipal();
String email = principal != null ? principal.getName() : null;
if (email == null || email.isBlank()) {
return Collections.emptyList();
}

View File

@@ -141,7 +141,7 @@ public class TransactionEpargneService {
alerteLcbFtService.genererAlerteSeuilDepasse(
orgId,
membreId,
request.getTypeTransaction() != null ? request.getTypeTransaction().name() : null,
request.getTypeTransaction().name(),
request.getMontant(),
seuil,
transaction.getId() != null ? transaction.getId().toString() : null,