package dev.lions.unionflow.server.service; import dev.lions.unionflow.server.entity.ApproverAction; import dev.lions.unionflow.server.entity.Membre; import dev.lions.unionflow.server.entity.Organisation; import dev.lions.unionflow.server.entity.TransactionApproval; import dev.lions.unionflow.server.repository.MembreRepository; import dev.lions.unionflow.server.repository.OrganisationRepository; import dev.lions.unionflow.server.repository.TransactionApprovalRepository; import dev.lions.unionflow.server.api.dto.finance_workflow.request.ApproveTransactionRequest; import dev.lions.unionflow.server.api.dto.finance_workflow.request.RejectTransactionRequest; import dev.lions.unionflow.server.api.dto.finance_workflow.response.ApproverActionResponse; import dev.lions.unionflow.server.api.dto.finance_workflow.response.TransactionApprovalResponse; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.transaction.Transactional; import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.ForbiddenException; import org.eclipse.microprofile.jwt.JsonWebToken; import org.jboss.logging.Logger; import java.time.LocalDateTime; import java.util.List; import java.util.UUID; import java.util.stream.Collectors; /** * Service métier pour la gestion des approbations de transactions * * @author UnionFlow Team * @version 1.0 * @since 2026-03-13 */ @ApplicationScoped public class ApprovalService { private static final Logger LOG = Logger.getLogger(ApprovalService.class); @Inject TransactionApprovalRepository approvalRepository; @Inject MembreRepository membreRepository; @Inject OrganisationRepository organisationRepository; @Inject JsonWebToken jwt; /** * Demande une approbation pour une transaction */ @Transactional public TransactionApprovalResponse requestApproval( UUID transactionId, String transactionType, Double amount, UUID organizationId) { LOG.infof("Demande d'approbation pour transaction %s (type: %s, montant: %.2f, org: %s)", transactionId, transactionType, amount, organizationId); // Récupérer l'utilisateur courant String userEmail = jwt.getClaim("email"); UUID userId = UUID.fromString(jwt.getClaim("sub")); Membre membre = membreRepository.findByEmail(userEmail) .orElseThrow(() -> new ForbiddenException("Utilisateur non trouvé")); // Récupérer l'organisation si fournie Organisation organisation = null; if (organizationId != null) { organisation = organisationRepository.findByIdOptional(organizationId) .orElseThrow(() -> new NotFoundException("Organisation non trouvée: " + organizationId)); } // Déterminer le niveau d'approbation requis selon le montant String requiredLevel = determineRequiredLevel(amount); // Créer la demande d'approbation TransactionApproval approval = TransactionApproval.builder() .transactionId(transactionId) .transactionType(transactionType) .amount(java.math.BigDecimal.valueOf(amount)) .currency("XOF") .requesterId(userId) .requesterName(membre.getNom() + " " + membre.getPrenom()) .organisation(organisation) .requiredLevel(requiredLevel) .status("PENDING") .createdAt(LocalDateTime.now()) .expiresAt(LocalDateTime.now().plusDays(7)) // 7 jours par défaut .build(); approvalRepository.persist(approval); LOG.infof("Demande d'approbation créée avec ID: %s (niveau: %s, %d approbations requises)", approval.getId(), requiredLevel, approval.getRequiredApprovals()); return toResponse(approval); } /** * Détermine le niveau d'approbation requis selon le montant * Utilise les niveaux standard de l'entité: NONE, LEVEL1, LEVEL2, LEVEL3 */ private String determineRequiredLevel(Double amount) { if (amount >= 5_000_000) { // 5M XOF return "LEVEL3"; // 3 approbations (Board) } else if (amount >= 1_000_000) { // 1M XOF return "LEVEL2"; // 2 approbations (Director) } else if (amount >= 100_000) { // 100K XOF return "LEVEL1"; // 1 approbation (Manager) } else { return "NONE"; // Pas d'approbation requise } } /** * Récupère toutes les approbations en attente */ public List getPendingApprovals(UUID organizationId) { LOG.infof("Récupération des approbations en attente pour l'organisation %s", organizationId); List approvals = organizationId != null ? approvalRepository.findPendingByOrganisation(organizationId) : approvalRepository.findPending(); return approvals.stream() .map(this::toResponse) .collect(Collectors.toList()); } /** * Récupère une approbation par ID */ public TransactionApprovalResponse getApprovalById(UUID approvalId) { LOG.infof("Récupération de l'approbation %s", approvalId); TransactionApproval approval = approvalRepository.findByIdOptional(approvalId) .orElseThrow(() -> new NotFoundException("Approbation non trouvée: " + approvalId)); return toResponse(approval); } /** * Approuve une transaction */ @Transactional public TransactionApprovalResponse approveTransaction(UUID approvalId, ApproveTransactionRequest request) { LOG.infof("Approbation de la transaction %s", approvalId); // Récupérer l'approbation TransactionApproval approval = approvalRepository.findByIdOptional(approvalId) .orElseThrow(() -> new NotFoundException("Approbation non trouvée: " + approvalId)); // Vérifier que l'approbation est en attente if (!"PENDING".equals(approval.getStatus())) { throw new ForbiddenException("Cette approbation n'est plus en attente"); } // Vérifier que l'approbation n'est pas expirée if (approval.isExpired()) { approval.setStatus("EXPIRED"); approvalRepository.persist(approval); throw new ForbiddenException("Cette approbation est expirée"); } // Récupérer l'utilisateur courant String userEmail = jwt.getClaim("email"); UUID userId = UUID.fromString(jwt.getClaim("sub")); Membre membre = membreRepository.findByEmail(userEmail) .orElseThrow(() -> new ForbiddenException("Utilisateur non trouvé")); // Vérifier que l'utilisateur n'est pas le demandeur if (approval.getRequesterId().equals(userId)) { throw new ForbiddenException("Vous ne pouvez pas approuver votre propre demande"); } // Créer l'action d'approbation ApproverAction action = ApproverAction.builder() .approval(approval) .approverId(userId) .approverName(membre.getNom() + " " + membre.getPrenom()) .approverRole(jwt.getClaim("role")) // Récupérer le rôle depuis JWT .decision("APPROVED") .comment(request.getComment()) .decidedAt(LocalDateTime.now()) .build(); approval.addApproverAction(action); // Vérifier si toutes les approbations requises sont reçues if (approval.hasAllApprovals()) { approval.setStatus("VALIDATED"); approval.setCompletedAt(LocalDateTime.now()); LOG.infof("Transaction %s validée avec toutes les approbations", approval.getTransactionId()); } else { approval.setStatus("APPROVED"); LOG.infof("Transaction %s approuvée (%d/%d)", approval.getTransactionId(), approval.countApprovals(), approval.getRequiredApprovals()); } approvalRepository.persist(approval); return toResponse(approval); } /** * Rejette une transaction */ @Transactional public TransactionApprovalResponse rejectTransaction(UUID approvalId, RejectTransactionRequest request) { LOG.infof("Rejet de la transaction %s", approvalId); // Récupérer l'approbation TransactionApproval approval = approvalRepository.findByIdOptional(approvalId) .orElseThrow(() -> new NotFoundException("Approbation non trouvée: " + approvalId)); // Vérifier que l'approbation est en attente if (!"PENDING".equals(approval.getStatus()) && !"APPROVED".equals(approval.getStatus())) { throw new ForbiddenException("Cette approbation ne peut plus être rejetée"); } // Récupérer l'utilisateur courant String userEmail = jwt.getClaim("email"); UUID userId = UUID.fromString(jwt.getClaim("sub")); Membre membre = membreRepository.findByEmail(userEmail) .orElseThrow(() -> new ForbiddenException("Utilisateur non trouvé")); // Créer l'action de rejet ApproverAction action = ApproverAction.builder() .approval(approval) .approverId(userId) .approverName(membre.getNom() + " " + membre.getPrenom()) .approverRole(jwt.getClaim("role")) .decision("REJECTED") .comment(request.getReason()) .decidedAt(LocalDateTime.now()) .build(); approval.addApproverAction(action); approval.setStatus("REJECTED"); approval.setRejectionReason(request.getReason()); approval.setCompletedAt(LocalDateTime.now()); approvalRepository.persist(approval); LOG.infof("Transaction %s rejetée: %s", approval.getTransactionId(), request.getReason()); return toResponse(approval); } /** * Récupère l'historique des approbations */ public List getApprovalsHistory( UUID organizationId, LocalDateTime startDate, LocalDateTime endDate, String status) { LOG.infof("Récupération de l'historique des approbations pour l'organisation %s", organizationId); if (organizationId == null) { throw new IllegalArgumentException("L'ID de l'organisation est requis"); } List approvals = approvalRepository.findHistory( organizationId, startDate, endDate, status); return approvals.stream() .map(this::toResponse) .collect(Collectors.toList()); } /** * Compte les approbations en attente */ public long countPendingApprovals(UUID organizationId) { if (organizationId == null) { return approvalRepository.count("status", "PENDING"); } return approvalRepository.countPendingByOrganisation(organizationId); } /** * Convertit une entité en DTO de réponse */ private TransactionApprovalResponse toResponse(TransactionApproval approval) { List approversResponse = approval.getApprovers().stream() .map(action -> ApproverActionResponse.builder() .id(action.getId()) .approverId(action.getApproverId()) .approverName(action.getApproverName()) .approverRole(action.getApproverRole()) .decision(action.getDecision()) .comment(action.getComment()) .decidedAt(action.getDecidedAt()) .build()) .collect(Collectors.toList()); return TransactionApprovalResponse.builder() .id(approval.getId()) .transactionId(approval.getTransactionId()) .transactionType(approval.getTransactionType()) .amount(approval.getAmount()) .currency(approval.getCurrency()) .requesterId(approval.getRequesterId()) .requesterName(approval.getRequesterName()) .organizationId(approval.getOrganisation() != null ? approval.getOrganisation().getId() : null) .requiredLevel(approval.getRequiredLevel()) .status(approval.getStatus()) .approvers(approversResponse) .rejectionReason(approval.getRejectionReason()) .createdAt(approval.getCreatedAt()) .expiresAt(approval.getExpiresAt()) .completedAt(approval.getCompletedAt()) .metadata(approval.getMetadata()) // Champs calculés .approvalCount((int) approval.countApprovals()) .requiredApprovals(approval.getRequiredApprovals()) .hasAllApprovals(approval.hasAllApprovals()) .isExpired(approval.isExpired()) .isPending(approval.isPending()) .isCompleted(approval.isCompleted()) .build(); } }