feat(backend): consolidation finale Spec 001 LCB-FT + Flyway V1-V5

Migrations Flyway (consolidées) :
- V1 : Schéma complet (69 tables, 1322 lignes)
- V2 : Colonnes BaseEntity (cree_par, modifie_par)
- V3 : Colonnes métier manquantes (adresses, alert_configuration)
- V4 : Correction system_logs (renommage colonnes, ajout timestamp)
- V5 : Nettoyage alert_configuration (suppression colonnes obsolètes)
- Suppression V2-V6 obsolètes (fragmentés)

Entités LCB-FT :
- AlerteLcbFt : Alertes anti-blanchiment
- AlertConfiguration : Configuration alertes
- SystemAlert : Alertes système
- SystemLog : Logs techniques (DÉJÀ COMMITÉE avec super.onCreate fix)

Services LCB-FT (T015, T016) :
- AlerteLcbFtService + Resource : Dashboard alertes admin
- AlertMonitoringService : Surveillance transactions
- SystemLoggingService : Logs centralisés
- FileStorageService : Upload documents

Repositories :
- AlerteLcbFtRepository
- AlertConfigurationRepository
- SystemAlertRepository
- SystemLogRepository

Tests :
- GlobalExceptionMapperTest : 17 erreurs corrigées (toResponse())

Spec 001 : 27/27 tâches (100%)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
dahoud
2026-03-16 05:15:17 +00:00
parent d8e3f23ec4
commit 347d89cc02
22 changed files with 3668 additions and 3129 deletions

View File

@@ -0,0 +1,161 @@
package dev.lions.unionflow.server.resource;
import dev.lions.unionflow.server.api.dto.lcbft.AlerteLcbFtResponse;
import dev.lions.unionflow.server.entity.AlerteLcbFt;
import dev.lions.unionflow.server.repository.AlerteLcbFtRepository;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* API REST pour la gestion des alertes LCB-FT.
*
* @author UnionFlow Team
* @version 1.0
* @since 2026-03-15
*/
@Path("/api/alertes-lcb-ft")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Alertes LCB-FT", description = "Gestion des alertes Lutte Contre le Blanchiment")
public class AlerteLcbFtResource {
@Inject
AlerteLcbFtRepository alerteLcbFtRepository;
/**
* Récupère les alertes LCB-FT avec filtres et pagination.
*/
@GET
@RolesAllowed({"ORG_ADMIN", "SUPER_ADMIN"})
@Operation(summary = "Liste des alertes LCB-FT", description = "Récupère les alertes avec filtrage et pagination")
public Response getAlertes(
@QueryParam("organisationId") String organisationId,
@QueryParam("typeAlerte") String typeAlerte,
@QueryParam("traitee") Boolean traitee,
@QueryParam("dateDebut") String dateDebut,
@QueryParam("dateFin") String dateFin,
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("20") int size
) {
UUID orgId = organisationId != null && !organisationId.isBlank() ? UUID.fromString(organisationId) : null;
LocalDateTime debut = dateDebut != null && !dateDebut.isBlank() ? LocalDateTime.parse(dateDebut) : null;
LocalDateTime fin = dateFin != null && !dateFin.isBlank() ? LocalDateTime.parse(dateFin) : null;
List<AlerteLcbFt> alertes = alerteLcbFtRepository.search(
orgId,
typeAlerte,
traitee,
debut,
fin,
page,
size
);
long total = alerteLcbFtRepository.count(orgId, typeAlerte, traitee, debut, fin);
List<AlerteLcbFtResponse> responses = alertes.stream()
.map(this::mapToResponse)
.collect(Collectors.toList());
Map<String, Object> result = new HashMap<>();
result.put("content", responses);
result.put("totalElements", total);
result.put("totalPages", (int) Math.ceil((double) total / size));
result.put("currentPage", page);
result.put("pageSize", size);
return Response.ok(result).build();
}
/**
* Récupère une alerte par son ID.
*/
@GET
@Path("/{id}")
@RolesAllowed({"ORG_ADMIN", "SUPER_ADMIN"})
@Operation(summary = "Détails d'une alerte", description = "Récupère une alerte par son ID")
public Response getAlerteById(@PathParam("id") String id) {
AlerteLcbFt alerte = alerteLcbFtRepository.findById(UUID.fromString(id));
if (alerte == null) {
throw new NotFoundException("Alerte non trouvée");
}
return Response.ok(mapToResponse(alerte)).build();
}
/**
* Marque une alerte comme traitée.
*/
@POST
@Path("/{id}/traiter")
@RolesAllowed({"ORG_ADMIN", "SUPER_ADMIN"})
@Operation(summary = "Traiter une alerte", description = "Marque une alerte comme traitée avec un commentaire")
public Response traiterAlerte(
@PathParam("id") String id,
Map<String, String> body
) {
AlerteLcbFt alerte = alerteLcbFtRepository.findById(UUID.fromString(id));
if (alerte == null) {
throw new NotFoundException("Alerte non trouvée");
}
alerte.setTraitee(true);
alerte.setDateTraitement(LocalDateTime.now());
alerte.setTraitePar(body.get("traitePar"));
alerte.setCommentaireTraitement(body.get("commentaire"));
alerteLcbFtRepository.persist(alerte);
return Response.ok(mapToResponse(alerte)).build();
}
/**
* Compte les alertes non traitées.
*/
@GET
@Path("/stats/non-traitees")
@RolesAllowed({"ORG_ADMIN", "SUPER_ADMIN"})
@Operation(summary = "Statistiques alertes", description = "Nombre d'alertes non traitées")
public Response getStatsNonTraitees(@QueryParam("organisationId") String organisationId) {
UUID orgId = organisationId != null && !organisationId.isBlank() ? UUID.fromString(organisationId) : null;
long count = alerteLcbFtRepository.countNonTraitees(orgId);
return Response.ok(Map.of("count", count)).build();
}
private AlerteLcbFtResponse mapToResponse(AlerteLcbFt alerte) {
return AlerteLcbFtResponse.builder()
.id(alerte.getId().toString())
.organisationId(alerte.getOrganisation() != null ? alerte.getOrganisation().getId().toString() : null)
.organisationNom(alerte.getOrganisation() != null ? alerte.getOrganisation().getNom() : null)
.membreId(alerte.getMembre() != null ? alerte.getMembre().getId().toString() : null)
.membreNomComplet(alerte.getMembre() != null ?
alerte.getMembre().getPrenom() + " " + alerte.getMembre().getNom() : null)
.typeAlerte(alerte.getTypeAlerte())
.dateAlerte(alerte.getDateAlerte())
.description(alerte.getDescription())
.details(alerte.getDetails())
.montant(alerte.getMontant())
.seuil(alerte.getSeuil())
.typeOperation(alerte.getTypeOperation())
.transactionRef(alerte.getTransactionRef())
.severite(alerte.getSeverite())
.traitee(alerte.getTraitee())
.dateTraitement(alerte.getDateTraitement())
.traitePar(alerte.getTraitePar())
.commentaireTraitement(alerte.getCommentaireTraitement())
.build();
}
}

View File

@@ -5,12 +5,18 @@ import dev.lions.unionflow.server.api.dto.document.response.DocumentResponse;
import dev.lions.unionflow.server.api.dto.document.request.CreatePieceJointeRequest;
import dev.lions.unionflow.server.api.dto.document.response.PieceJointeResponse;
import dev.lions.unionflow.server.service.DocumentService;
import dev.lions.unionflow.server.service.FileStorageService;
import dev.lions.unionflow.server.api.enums.document.TypeDocument;
import dev.lions.unionflow.server.entity.Document;
import dev.lions.unionflow.server.repository.DocumentRepository;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import org.jboss.resteasy.reactive.multipart.FileUpload;
import jakarta.validation.Valid;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.nio.file.Files;
import java.util.List;
import java.util.UUID;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
@@ -35,6 +41,12 @@ public class DocumentResource {
@Inject
DocumentService documentService;
@Inject
FileStorageService fileStorageService;
@Inject
DocumentRepository documentRepository;
/**
* Crée un nouveau document
*
@@ -55,6 +67,84 @@ public class DocumentResource {
}
}
/**
* Upload un fichier (image ou PDF) pour justificatif LCB-FT
*
* @param file Fichier uploadé
* @param description Description optionnelle
* @return ID du document créé
*/
@POST
@Path("/upload")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@RolesAllowed({ "ADMIN", "MEMBRE" })
@jakarta.transaction.Transactional
public Response uploadFile(
@org.jboss.resteasy.reactive.RestForm("file") FileUpload file,
@org.jboss.resteasy.reactive.RestForm("description") String description,
@org.jboss.resteasy.reactive.RestForm("typeDocument") String typeDocument
) {
try {
if (file == null || file.fileName() == null) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Aucun fichier fourni"))
.build();
}
LOG.infof("Upload de fichier: %s (%d octets, type: %s)",
file.fileName(), file.size(), file.contentType());
// Stocker le fichier physiquement
FileStorageService.FileMetadata metadata;
try (var inputStream = Files.newInputStream(file.filePath())) {
metadata = fileStorageService.storeFile(
inputStream,
file.fileName(),
file.contentType(),
file.size()
);
}
// Créer l'entité Document en BDD
Document document = Document.builder()
.nomFichier(metadata.getNomFichier())
.nomOriginal(metadata.getNomOriginal())
.cheminStockage(metadata.getCheminStockage())
.typeMime(metadata.getTypeMime())
.tailleOctets(metadata.getTailleOctets())
.hashMd5(metadata.getHashMd5())
.hashSha256(metadata.getHashSha256())
.description(description)
.typeDocument(typeDocument != null ? TypeDocument.valueOf(typeDocument) : TypeDocument.PIECE_JUSTIFICATIVE)
.build();
documentRepository.persist(document);
LOG.infof("Document créé avec ID: %s", document.getId());
// Retourner l'ID du document (pour référencer dans TransactionEpargneRequest)
return Response.status(Response.Status.CREATED)
.entity(java.util.Map.of(
"id", document.getId().toString(),
"nomFichier", document.getNomFichier(),
"taille", document.getTailleFormatee(),
"typeMime", document.getTypeMime()
))
.build();
} catch (IllegalArgumentException e) {
LOG.warnf("Validation échouée pour upload: %s", e.getMessage());
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse(e.getMessage()))
.build();
} catch (Exception e) {
LOG.errorf(e, "Erreur lors de l'upload du fichier");
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse("Erreur lors de l'upload: " + e.getMessage()))
.build();
}
}
/**
* Trouve un document par son ID
*

View File

@@ -3,7 +3,7 @@ package dev.lions.unionflow.server.resource;
import dev.lions.unionflow.server.api.dto.system.request.UpdateSystemConfigRequest;
import dev.lions.unionflow.server.api.dto.system.response.CacheStatsResponse;
import dev.lions.unionflow.server.api.dto.system.response.SystemConfigResponse;
import dev.lions.unionflow.server.api.dto.system.response.SystemMetricsResponse;
import dev.lions.unionflow.server.api.dto.logs.response.SystemMetricsResponse;
import dev.lions.unionflow.server.api.dto.system.response.SystemTestResultResponse;
import dev.lions.unionflow.server.service.SystemConfigService;
import dev.lions.unionflow.server.service.SystemMetricsService;