Sync: code local unifié
Synchronisation du code source local (fait foi). Signed-off-by: lions dev Team
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user