From a6ad4c9aea53735535b8c38bb8bdfdcfbb50ab12 Mon Sep 17 00:00:00 2001 From: dahoud <41957584+DahoudG@users.noreply.github.com> Date: Fri, 10 Apr 2026 20:53:19 +0000 Subject: [PATCH] fix: BUG-01 + AUTH + DATA-01 UnionFlow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../server/service/BudgetService.java | 2 +- .../service/MembreKeycloakSyncService.java | 18 +++-- .../server/service/MembreService.java | 10 +++ .../service/BudgetServiceCoverageTest.java | 68 +++++++++++++++++++ 4 files changed, 91 insertions(+), 7 deletions(-) diff --git a/src/main/java/dev/lions/unionflow/server/service/BudgetService.java b/src/main/java/dev/lions/unionflow/server/service/BudgetService.java index 435718a..320c4b7 100644 --- a/src/main/java/dev/lions/unionflow/server/service/BudgetService.java +++ b/src/main/java/dev/lions/unionflow/server/service/BudgetService.java @@ -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()) diff --git a/src/main/java/dev/lions/unionflow/server/service/MembreKeycloakSyncService.java b/src/main/java/dev/lions/unionflow/server/service/MembreKeycloakSyncService.java index a5addc9..e095a9e 100644 --- a/src/main/java/dev/lions/unionflow/server/service/MembreKeycloakSyncService.java +++ b/src/main/java/dev/lions/unionflow/server/service/MembreKeycloakSyncService.java @@ -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); diff --git a/src/main/java/dev/lions/unionflow/server/service/MembreService.java b/src/main/java/dev/lions/unionflow/server/service/MembreService.java index e5dceb0..272f8f7 100644 --- a/src/main/java/dev/lions/unionflow/server/service/MembreService.java +++ b/src/main/java/dev/lions/unionflow/server/service/MembreService.java @@ -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()); } diff --git a/src/test/java/dev/lions/unionflow/server/service/BudgetServiceCoverageTest.java b/src/test/java/dev/lions/unionflow/server/service/BudgetServiceCoverageTest.java index 24d98c5..7c3380a 100644 --- a/src/test/java/dev/lions/unionflow/server/service/BudgetServiceCoverageTest.java +++ b/src/test/java/dev/lions/unionflow/server/service/BudgetServiceCoverageTest.java @@ -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); + } }