diff --git a/src/main/java/dev/lions/unionflow/server/repository/AuditTrailOperationRepository.java b/src/main/java/dev/lions/unionflow/server/repository/AuditTrailOperationRepository.java index 52ce672..58cbe60 100644 --- a/src/main/java/dev/lions/unionflow/server/repository/AuditTrailOperationRepository.java +++ b/src/main/java/dev/lions/unionflow/server/repository/AuditTrailOperationRepository.java @@ -47,4 +47,21 @@ public class AuditTrailOperationRepository + "ORDER BY operationAt DESC", organisationId, from, to); } + + /** N opérations les plus récentes — toutes organisations confondues (Live Feed). */ + public List findRecent(int limit) { + return find("ORDER BY operationAt DESC").page(0, Math.max(1, Math.min(limit, 500))).list(); + } + + /** N opérations les plus récentes pour une organisation. */ + public List findRecentByOrganisation(UUID organisationId, int limit) { + return find("organisationActiveId = ?1 ORDER BY operationAt DESC", organisationId) + .page(0, Math.max(1, Math.min(limit, 500))).list(); + } + + /** N opérations les plus récentes d'un utilisateur. */ + public List findRecentByUser(UUID userId, int limit) { + return find("userId = ?1 ORDER BY operationAt DESC", userId) + .page(0, Math.max(1, Math.min(limit, 500))).list(); + } } diff --git a/src/main/java/dev/lions/unionflow/server/resource/AuditTrailOperationResource.java b/src/main/java/dev/lions/unionflow/server/resource/AuditTrailOperationResource.java index 81a9a5e..62484d5 100644 --- a/src/main/java/dev/lions/unionflow/server/resource/AuditTrailOperationResource.java +++ b/src/main/java/dev/lions/unionflow/server/resource/AuditTrailOperationResource.java @@ -78,6 +78,30 @@ public class AuditTrailOperationResource { return queryService.operationsFinancieres(orgId, fromDt, toDt); } + /** + * Live Activity Feed (Sprint 15) — N opérations les plus récentes (transparency). + * + *

Scopes : + *

    + *
  • ALL — toutes orgs (restreint compliance/contrôleurs/super-admin)
  • + *
  • ORG — organisation indiquée (admin org / président / officers)
  • + *
  • SELF (défaut) — opérations de l'utilisateur indiqué (n'importe quel rôle peut voir les siennes)
  • + *
+ * + *

Limit clampé à [1, 500] côté repository (sécurité contre DoS). + */ + @GET + @Path("/recent") + @RolesAllowed({"COMPLIANCE_OFFICER", "CONTROLEUR_INTERNE", "ADMIN_ORGANISATION", + "PRESIDENT", "TRESORIER", "MEMBRE", "SUPER_ADMIN"}) + public List recent( + @QueryParam("scope") @jakarta.ws.rs.DefaultValue("SELF") String scope, + @QueryParam("orgId") UUID orgId, + @QueryParam("userId") UUID userId, + @QueryParam("limit") @jakarta.ws.rs.DefaultValue("50") int limit) { + return queryService.listerRecentes(scope, orgId, userId, limit); + } + private LocalDateTime parseDateTime(String input, LocalDateTime fallback) { if (input == null || input.isBlank()) return fallback; try { diff --git a/src/main/java/dev/lions/unionflow/server/service/audit/AuditTrailQueryService.java b/src/main/java/dev/lions/unionflow/server/service/audit/AuditTrailQueryService.java index 1e4d1f6..7c6b722 100644 --- a/src/main/java/dev/lions/unionflow/server/service/audit/AuditTrailQueryService.java +++ b/src/main/java/dev/lions/unionflow/server/service/audit/AuditTrailQueryService.java @@ -52,6 +52,32 @@ public class AuditTrailQueryService { .map(this::toResponse).toList(); } + /** + * Live Feed (Sprint 15) — N opérations les plus récentes selon le scope demandé. + * + * @param scope ALL (toutes orgs — restreint COMPLIANCE_OFFICER/CONTROLEUR_INTERNE/SUPER_ADMIN), + * ORG (organisation passée), SELF (utilisateur passé) + * @param organisationId requis pour scope=ORG + * @param userId requis pour scope=SELF + * @param limit clampé à [1, 500] + */ + public List listerRecentes( + String scope, UUID organisationId, UUID userId, int limit) { + String s = scope == null ? "ALL" : scope.toUpperCase(); + return switch (s) { + case "ORG" -> organisationId == null + ? java.util.List.of() + : repository.findRecentByOrganisation(organisationId, limit) + .stream().map(this::toResponse).toList(); + case "SELF" -> userId == null + ? java.util.List.of() + : repository.findRecentByUser(userId, limit) + .stream().map(this::toResponse).toList(); + default -> repository.findRecent(limit) + .stream().map(this::toResponse).toList(); + }; + } + // ──────────────────────────────────────────────────────────── // Mapping Entity ↔ DTO // ──────────────────────────────────────────────────────────── diff --git a/src/test/java/dev/lions/unionflow/server/service/audit/AuditTrailQueryServiceTest.java b/src/test/java/dev/lions/unionflow/server/service/audit/AuditTrailQueryServiceTest.java index 1eec9e5..3e42fcf 100644 --- a/src/test/java/dev/lions/unionflow/server/service/audit/AuditTrailQueryServiceTest.java +++ b/src/test/java/dev/lions/unionflow/server/service/audit/AuditTrailQueryServiceTest.java @@ -97,4 +97,65 @@ class AuditTrailQueryServiceTest { assertThat(result).hasSize(1); assertThat(result.get(0).entityType()).isEqualTo("Membre"); } + + // ── Sprint 15 : Live Activity Feed (listerRecentes) ───────────────────────── + + @Test + @DisplayName("listerRecentes scope=ALL appelle findRecent") + void recentes_All() { + when(repository.findRecent(50)) + .thenReturn(List.of(op("Membre", "CREATE", true), op("Cotisation", "UPDATE", true))); + var result = service.listerRecentes("ALL", null, null, 50); + assertThat(result).hasSize(2); + } + + @Test + @DisplayName("listerRecentes scope=null → ALL par défaut") + void recentes_NullScopeDefaultsToAll() { + when(repository.findRecent(20)).thenReturn(List.of(op("X", "CREATE", true))); + var result = service.listerRecentes(null, null, null, 20); + assertThat(result).hasSize(1); + } + + @Test + @DisplayName("listerRecentes scope=ORG appelle findRecentByOrganisation") + void recentes_Org() { + UUID orgId = UUID.randomUUID(); + when(repository.findRecentByOrganisation(orgId, 30)) + .thenReturn(List.of(op("Cotisation", "VALIDATE", true))); + var result = service.listerRecentes("ORG", orgId, null, 30); + assertThat(result).hasSize(1); + } + + @Test + @DisplayName("listerRecentes scope=ORG sans orgId → liste vide") + void recentes_OrgWithoutId() { + var result = service.listerRecentes("ORG", null, null, 30); + assertThat(result).isEmpty(); + } + + @Test + @DisplayName("listerRecentes scope=SELF appelle findRecentByUser") + void recentes_Self() { + UUID userId = UUID.randomUUID(); + when(repository.findRecentByUser(userId, 25)) + .thenReturn(List.of(op("Profil", "UPDATE", true))); + var result = service.listerRecentes("SELF", null, userId, 25); + assertThat(result).hasSize(1); + } + + @Test + @DisplayName("listerRecentes scope=SELF sans userId → liste vide") + void recentes_SelfWithoutUser() { + var result = service.listerRecentes("SELF", null, null, 25); + assertThat(result).isEmpty(); + } + + @Test + @DisplayName("listerRecentes scope insensible à la casse") + void recentes_CaseInsensitive() { + when(repository.findRecent(10)).thenReturn(List.of(op("X", "CREATE", true))); + var result = service.listerRecentes("all", null, null, 10); + assertThat(result).hasSize(1); + } }