Sync: code local unifié

Synchronisation du code source local (fait foi).

Signed-off-by: lions dev Team
This commit is contained in:
dahoud
2026-03-15 16:25:40 +00:00
parent e82dc356f3
commit 75a19988b0
730 changed files with 53599 additions and 13145 deletions

View File

@@ -0,0 +1,277 @@
package dev.lions.unionflow.server.service;
import dev.lions.unionflow.server.entity.Budget;
import dev.lions.unionflow.server.entity.BudgetLine;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.repository.BudgetRepository;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import dev.lions.unionflow.server.api.dto.finance_workflow.request.CreateBudgetLineRequest;
import dev.lions.unionflow.server.api.dto.finance_workflow.request.CreateBudgetRequest;
import dev.lions.unionflow.server.api.dto.finance_workflow.response.BudgetLineResponse;
import dev.lions.unionflow.server.api.dto.finance_workflow.response.BudgetResponse;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.BadRequestException;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.jboss.logging.Logger;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
/**
* Service métier pour la gestion des budgets
*
* @author UnionFlow Team
* @version 1.0
* @since 2026-03-13
*/
@ApplicationScoped
public class BudgetService {
private static final Logger LOG = Logger.getLogger(BudgetService.class);
@Inject
BudgetRepository budgetRepository;
@Inject
OrganisationRepository organisationRepository;
@Inject
JsonWebToken jwt;
/**
* Récupère tous les budgets d'une organisation avec filtres optionnels
*/
public List<BudgetResponse> getBudgets(UUID organizationId, String status, Integer year) {
LOG.infof("Récupération des budgets pour l'organisation %s (status=%s, year=%s)",
organizationId, status, year);
if (organizationId == null) {
throw new BadRequestException("L'ID de l'organisation est requis");
}
List<Budget> budgets = budgetRepository.findByOrganisationWithFilters(
organizationId, status, year);
return budgets.stream()
.map(this::toResponse)
.collect(Collectors.toList());
}
/**
* Récupère un budget par ID
*/
public BudgetResponse getBudgetById(UUID budgetId) {
LOG.infof("Récupération du budget %s", budgetId);
Budget budget = budgetRepository.findByIdOptional(budgetId)
.orElseThrow(() -> new NotFoundException("Budget non trouvé: " + budgetId));
return toResponse(budget);
}
/**
* Crée un nouveau budget
*/
@Transactional
public BudgetResponse createBudget(CreateBudgetRequest request) {
LOG.infof("Création d'un budget: %s", request.getName());
// Vérifier que l'organisation existe
Organisation organisation = organisationRepository.findByIdOptional(request.getOrganizationId())
.orElseThrow(() -> new NotFoundException("Organisation non trouvée: " + request.getOrganizationId()));
// Valider la période
if ("MONTHLY".equals(request.getPeriod()) && request.getMonth() == null) {
throw new BadRequestException("Le mois est requis pour un budget mensuel");
}
// Calculer les dates de début et fin
LocalDate startDate = calculateStartDate(request.getPeriod(), request.getYear(), request.getMonth());
LocalDate endDate = calculateEndDate(request.getPeriod(), request.getYear(), request.getMonth());
// Récupérer l'utilisateur courant
UUID userId = UUID.fromString(jwt.getClaim("sub"));
// Créer le budget
Budget budget = Budget.builder()
.name(request.getName())
.description(request.getDescription())
.organisation(organisation)
.period(request.getPeriod())
.year(request.getYear())
.month(request.getMonth())
.status("DRAFT")
.currency(request.getCurrency() != null ? request.getCurrency() : "XOF")
.createdById(userId)
.createdAtBudget(LocalDateTime.now())
.startDate(startDate)
.endDate(endDate)
.build();
// Ajouter les lignes budgétaires
for (CreateBudgetLineRequest lineRequest : request.getLines()) {
BudgetLine line = BudgetLine.builder()
.budget(budget)
.category(lineRequest.getCategory())
.name(lineRequest.getName())
.description(lineRequest.getDescription())
.amountPlanned(lineRequest.getAmountPlanned())
.amountRealized(BigDecimal.ZERO)
.notes(lineRequest.getNotes())
.build();
budget.addLine(line);
}
// Persister le budget
budgetRepository.persist(budget);
LOG.infof("Budget créé avec ID: %s", budget.getId());
return toResponse(budget);
}
/**
* Récupère le suivi budgétaire (tracking)
*/
public Map<String, Object> getBudgetTracking(UUID budgetId) {
LOG.infof("Récupération du suivi budgétaire pour %s", budgetId);
Budget budget = budgetRepository.findByIdOptional(budgetId)
.orElseThrow(() -> new NotFoundException("Budget non trouvé: " + budgetId));
Map<String, Object> tracking = new HashMap<>();
tracking.put("budgetId", budget.getId());
tracking.put("name", budget.getName());
tracking.put("status", budget.getStatus());
tracking.put("totalPlanned", budget.getTotalPlanned());
tracking.put("totalRealized", budget.getTotalRealized());
tracking.put("realizationRate", budget.getRealizationRate());
tracking.put("variance", budget.getVariance());
tracking.put("isOverBudget", budget.isOverBudget());
tracking.put("isActive", budget.isActive());
tracking.put("isCurrentPeriod", budget.isCurrentPeriod());
// Tracking par catégorie
Map<String, Map<String, Object>> byCategory = new HashMap<>();
for (BudgetLine line : budget.getLines()) {
String category = line.getCategory();
Map<String, Object> categoryData = byCategory.getOrDefault(category, new HashMap<>());
BigDecimal planned = (BigDecimal) categoryData.getOrDefault("planned", BigDecimal.ZERO);
BigDecimal realized = (BigDecimal) categoryData.getOrDefault("realized", BigDecimal.ZERO);
categoryData.put("planned", planned.add(line.getAmountPlanned()));
categoryData.put("realized", realized.add(line.getAmountRealized()));
byCategory.put(category, categoryData);
}
tracking.put("byCategory", byCategory);
// Lignes avec le plus grand écart
List<Map<String, Object>> topVariances = budget.getLines().stream()
.sorted((l1, l2) -> l2.getVariance().abs().compareTo(l1.getVariance().abs()))
.limit(5)
.map(line -> {
Map<String, Object> lineData = new HashMap<>();
lineData.put("name", line.getName());
lineData.put("category", line.getCategory());
lineData.put("planned", line.getAmountPlanned());
lineData.put("realized", line.getAmountRealized());
lineData.put("variance", line.getVariance());
lineData.put("isOverBudget", line.isOverBudget());
return lineData;
})
.collect(Collectors.toList());
tracking.put("topVariances", topVariances);
return tracking;
}
/**
* Calcule la date de début selon la période
*/
private LocalDate calculateStartDate(String period, int year, Integer month) {
return switch (period) {
case "MONTHLY" -> LocalDate.of(year, month != null ? month : 1, 1);
case "QUARTERLY" -> LocalDate.of(year, 1, 1); // Simplification: Q1
case "SEMIANNUAL" -> LocalDate.of(year, 1, 1); // Simplification: S1
case "ANNUAL" -> LocalDate.of(year, 1, 1);
default -> LocalDate.of(year, 1, 1);
};
}
/**
* Calcule la date de fin selon la période
*/
private LocalDate calculateEndDate(String period, int year, Integer month) {
return switch (period) {
case "MONTHLY" -> {
int m = month != null ? month : 1;
yield LocalDate.of(year, m, 1).plusMonths(1).minusDays(1);
}
case "QUARTERLY" -> LocalDate.of(year, 3, 31); // Simplification: Q1
case "SEMIANNUAL" -> LocalDate.of(year, 6, 30); // Simplification: S1
case "ANNUAL" -> LocalDate.of(year, 12, 31);
default -> LocalDate.of(year, 12, 31);
};
}
/**
* Convertit une entité Budget en DTO de réponse
*/
private BudgetResponse toResponse(Budget budget) {
List<BudgetLineResponse> linesResponse = budget.getLines().stream()
.map(line -> BudgetLineResponse.builder()
.id(line.getId())
.category(line.getCategory())
.name(line.getName())
.description(line.getDescription())
.amountPlanned(line.getAmountPlanned())
.amountRealized(line.getAmountRealized())
.notes(line.getNotes())
// Champs calculés
.realizationRate(line.getRealizationRate())
.variance(line.getVariance())
.isOverBudget(line.isOverBudget())
.build())
.collect(Collectors.toList());
return BudgetResponse.builder()
.id(budget.getId())
.name(budget.getName())
.description(budget.getDescription())
.organizationId(budget.getOrganisation().getId())
.period(budget.getPeriod())
.year(budget.getYear())
.month(budget.getMonth())
.status(budget.getStatus())
.lines(linesResponse)
.totalPlanned(budget.getTotalPlanned())
.totalRealized(budget.getTotalRealized())
.currency(budget.getCurrency())
.createdById(budget.getCreatedById())
.createdAt(budget.getCreatedAtBudget())
.approvedAt(budget.getApprovedAt())
.approvedById(budget.getApprovedById())
.startDate(budget.getStartDate())
.endDate(budget.getEndDate())
.metadata(budget.getMetadata())
// Champs calculés
.realizationRate(budget.getRealizationRate())
.variance(budget.getVariance())
.varianceRate(budget.getVariance().doubleValue() / budget.getTotalPlanned().doubleValue() * 100)
.isOverBudget(budget.isOverBudget())
.isActive(budget.isActive())
.isCurrentPeriod(budget.isCurrentPeriod())
.build();
}
}