fix: BUG-01 + AUTH + DATA-01 UnionFlow

BUG-01: BudgetService.toResponse() — remplace doubleValue()>0 par
compareTo(BigDecimal.ZERO)>0 (précision BigDecimal) ; ajoute 2 tests
couvrant varianceRate=0 (totalPlanned=0) et varianceRate=-40%

AUTH: MembreKeycloakSyncService.changerMotDePassePremierLogin() — élargit
le catch de ForbiddenException vers WebApplicationException avec vérification
du statut HTTP (le REST client MicroProfile ne garantit pas la sous-classe)

DATA-01: MembreService.desactiverMembre() — décrémente nombreMembres sur
toutes les orgs actives du membre et passe le statutMembre à DESACTIVE
This commit is contained in:
dahoud
2026-04-10 20:53:19 +00:00
parent 5e21ef9573
commit a6ad4c9aea
4 changed files with 91 additions and 7 deletions

View File

@@ -322,7 +322,7 @@ public class BudgetService {
// Champs calculés
.realizationRate(budget.getRealizationRate())
.variance(budget.getVariance())
.varianceRate(budget.getTotalPlanned() != null && budget.getTotalPlanned().doubleValue() > 0
.varianceRate(budget.getTotalPlanned().compareTo(BigDecimal.ZERO) > 0
? budget.getVariance().doubleValue() / budget.getTotalPlanned().doubleValue() * 100
: 0.0)
.isOverBudget(budget.isOverBudget())

View File

@@ -533,14 +533,20 @@ public class MembreKeycloakSyncService {
.temporary(false)
.build();
// Tentative via lions-user-manager ; fallback sur l'API Admin Keycloak directe si 403
// Tentative via lions-user-manager ; fallback sur l'API Admin Keycloak directe si 403/503
// Note : le REST client MicroProfile peut lever WebApplicationException (pas nécessairement
// ForbiddenException) selon la configuration du mapper de réponse — on capture la classe mère.
try {
userServiceClient.resetPassword(keycloakUserId, DEFAULT_REALM, resetRequest);
} catch (jakarta.ws.rs.ForbiddenException | jakarta.ws.rs.ServiceUnavailableException e) {
LOGGER.warning("lions-user-manager reset-password échoué (" + e.getMessage()
+ "), fallback sur API Admin Keycloak directe.");
changerMotDePasseDirectKeycloak(membre.getId(), nouveauMotDePasse);
return; // changerMotDePasseDirectKeycloak persiste déjà les flags
} catch (jakarta.ws.rs.WebApplicationException e) {
int status = e.getResponse() != null ? e.getResponse().getStatus() : 0;
if (status == 403 || status == 503) {
LOGGER.warning("lions-user-manager reset-password échoué (HTTP " + status
+ "), fallback sur API Admin Keycloak directe.");
changerMotDePasseDirectKeycloak(membre.getId(), nouveauMotDePasse);
return; // changerMotDePasseDirectKeycloak persiste déjà les flags
}
throw e; // Statuts inattendus (400, 500…) : propager
}
membre.setPremiereConnexion(false);

View File

@@ -304,6 +304,16 @@ public class MembreService {
}
membre.setActif(false);
// Décrémenter le compteur nombreMembres pour chaque organisation active du membre
// (fix DATA-01 : le compteur restait figé lors d'une désactivation directe)
membreOrganisationRepository.findOrganisationsActivesParMembre(id).forEach(mo -> {
mo.getOrganisation().retirerMembre();
mo.setStatutMembre("DESACTIVE");
LOG.infof("Compteur membres décrémenté pour organisation %s (membre désactivé)",
mo.getOrganisation().getId());
});
LOG.infof("Membre désactivé: %s", membre.getNomComplet());
}

View File

@@ -6,8 +6,10 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import dev.lions.unionflow.server.api.dto.finance_workflow.request.CreateBudgetRequest;
import dev.lions.unionflow.server.api.dto.finance_workflow.request.CreateBudgetLineRequest;
import dev.lions.unionflow.server.api.dto.finance_workflow.response.BudgetResponse;
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;
@@ -15,7 +17,9 @@ import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Optional;
import java.util.UUID;
import org.eclipse.microprofile.jwt.JsonWebToken;
@@ -139,4 +143,68 @@ class BudgetServiceCoverageTest {
assertThat(result).isEqualTo(LocalDate.of(2026, 2, 28));
}
// =========================================================================
// toResponse — varianceRate : totalPlanned=0 → 0.0 (pas de division par zéro)
// =========================================================================
@Test
@DisplayName("varianceRate=0.0 quand totalPlanned=0 (guard BigDecimal.ZERO.compareTo)")
void toResponse_totalPlannedZero_varianceRateIsZero() {
// Budget sans ligne → totalPlanned=0, totalRealized=0
Budget b = Budget.builder()
.name("Budget Vide")
.organisation(org)
.period("ANNUAL")
.year(2026)
.currency("XOF")
.createdById(USER_ID)
.createdAtBudget(LocalDateTime.now())
.startDate(LocalDate.of(2026, 1, 1))
.endDate(LocalDate.of(2026, 12, 31))
.build();
b.setId(UUID.randomUUID());
when(budgetRepository.findByIdOptional(b.getId())).thenReturn(Optional.of(b));
BudgetResponse response = budgetService.getBudgetById(b.getId());
assertThat(response.getTotalPlanned()).isEqualByComparingTo(BigDecimal.ZERO);
assertThat(response.getVarianceRate()).isEqualTo(0.0);
}
@Test
@DisplayName("varianceRate calculé correctement quand totalPlanned > 0")
void toResponse_totalPlannedPositive_varianceRateCalculated() {
// Budget avec totalPlanned=500000, totalRealized=300000
// variance = 300000 - 500000 = -200000 → varianceRate = -200000/500000*100 = -40.0%
Budget b = Budget.builder()
.name("Budget avec lignes")
.organisation(org)
.period("ANNUAL")
.year(2026)
.currency("XOF")
.createdById(USER_ID)
.createdAtBudget(LocalDateTime.now())
.startDate(LocalDate.of(2026, 1, 1))
.endDate(LocalDate.of(2026, 12, 31))
.build();
b.setId(UUID.randomUUID());
BudgetLine line = BudgetLine.builder()
.budget(b)
.category("CONTRIBUTIONS")
.name("Cotisations")
.amountPlanned(BigDecimal.valueOf(500_000))
.amountRealized(BigDecimal.valueOf(300_000))
.build();
line.setId(UUID.randomUUID());
b.addLine(line);
when(budgetRepository.findByIdOptional(b.getId())).thenReturn(Optional.of(b));
BudgetResponse response = budgetService.getBudgetById(b.getId());
assertThat(response.getTotalPlanned()).isEqualByComparingTo(BigDecimal.valueOf(500_000));
assertThat(response.getVarianceRate()).isEqualTo(-40.0);
}
}