feat(sprint-5 P2-NEW-3 2026-04-25): reporting trimestriel ControleurInterne automatisé + scheduler + tests
Some checks failed
CI/CD Pipeline / pipeline (push) Failing after 3m26s

Rapport trimestriel agrégé pour le Contrôleur Interne (source AG + inspections BCEAO/ARTCI).

Entités & migration
- RapportTrimestrielControleurInterne (BaseEntity + JSONB contenu + bytea PDF + hash SHA-256)
- V48 : table rapports_trimestriels_controleur_interne, contraintes, indexes
- Repository : trouverParOrgAnneeTrimestre, listerParOrgAnnee, listerNonSignes

Service RapportTrimestrielService
- genererRapport(orgId, annee, trimestre) : agrège ComplianceSnapshot + DOS CENTIF + formations LBC/FT + anomalies SoD audit trail + demandes aide
- construireJson : structure conformite/activite/alertes
- genererPdf : OpenPDF A4 avec sections 1.Conformité 2.Activité 3.Alertes + bloc signature
- signer(rapportId, signataireId) : calcule SHA-256 du JSON, fige le statut
- archiver(rapportId) : passe SIGNE → ARCHIVE
- Idempotent en DRAFT (régénération possible) ; immuable en SIGNE/ARCHIVE

Resource RapportTrimestrielResource
- GET /api/rapports/trimestriel?orgId&annee — lister
- POST /api/rapports/trimestriel/generer — CONTROLEUR_INTERNE / SUPER_ADMIN
- POST /api/rapports/trimestriel/{id}/signer — CONTROLEUR_INTERNE
- POST /api/rapports/trimestriel/{id}/archiver — CONTROLEUR_INTERNE / PRESIDENT
- GET /api/rapports/trimestriel/{id}/pdf — application/pdf

Scheduler RapportTrimestrielScheduler
- @Scheduled cron 0 17 2 1 1,4,7,10 ? — 1er jan/avr/jul/oct à 02:17
- Génère pour toutes les orgs actives le trimestre précédent
- Override possible via unionflow.reporting.trimestriel.cron
- ConcurrentExecution.SKIP

RoleConstant
- Ajout CONTROLEUR_INTERNE, COMPLIANCE_OFFICER, COMMISSAIRE_COMPTES (utilisés depuis V45)

Tests Sprint 5 (20/20 verts)
- RapportTrimestrielServiceTest : 15 tests (debutTrimestre, finTrimestre, hash SHA-256, JSON alertes, PDF non vide)
- RapportTrimestrielSchedulerTest : 5 tests (trimestrePrecedent — incluant rollover année)
This commit is contained in:
dahoud
2026-04-25 10:13:07 +00:00
parent b0ee8881fb
commit a0b2690c17
9 changed files with 1029 additions and 0 deletions

View File

@@ -0,0 +1,88 @@
package dev.lions.unionflow.server.resource;
import dev.lions.unionflow.server.entity.RapportTrimestrielControleurInterne;
import dev.lions.unionflow.server.repository.RapportTrimestrielControleurInterneRepository;
import dev.lions.unionflow.server.security.OrganisationContextHolder;
import dev.lions.unionflow.server.service.reporting.RapportTrimestrielService;
import io.quarkus.security.Authenticated;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.List;
import java.util.UUID;
/**
* Endpoints du Reporting trimestriel ControleurInterne (P2-NEW-3).
*
* <p>Accès restreint au {@code CONTROLEUR_INTERNE} de l'organisation et au {@code SUPER_ADMIN}.
* La sélection de l'organisation active passe par le filtre {@link OrganisationContextHolder}
* (header {@code X-Active-Organisation-Id}) ou via paramètre {@code orgId} pour SUPER_ADMIN.
*/
@Path("/api/rapports/trimestriel")
@Produces(MediaType.APPLICATION_JSON)
@Authenticated
public class RapportTrimestrielResource {
@Inject RapportTrimestrielService service;
@Inject RapportTrimestrielControleurInterneRepository repository;
@Inject OrganisationContextHolder orgContext;
@GET
@RolesAllowed({"CONTROLEUR_INTERNE", "PRESIDENT", "ADMIN_ORGANISATION", "SUPER_ADMIN"})
public List<RapportTrimestrielControleurInterne> lister(
@QueryParam("orgId") UUID orgId,
@QueryParam("annee") Integer annee) {
UUID effectiveOrg = orgId != null ? orgId : orgContext.getOrganisationId();
int effectiveAnnee = annee != null ? annee : java.time.Year.now().getValue();
return repository.listerParOrgAnnee(effectiveOrg, effectiveAnnee);
}
@POST
@Path("/generer")
@RolesAllowed({"CONTROLEUR_INTERNE", "SUPER_ADMIN"})
public RapportTrimestrielControleurInterne generer(
@QueryParam("orgId") UUID orgId,
@QueryParam("annee") int annee,
@QueryParam("trimestre") int trimestre) {
UUID effectiveOrg = orgId != null ? orgId : orgContext.getOrganisationId();
return service.genererRapport(effectiveOrg, annee, trimestre);
}
@POST
@Path("/{id}/signer")
@RolesAllowed({"CONTROLEUR_INTERNE", "SUPER_ADMIN"})
public RapportTrimestrielControleurInterne signer(
@PathParam("id") UUID id, @QueryParam("signataireId") UUID signataireId) {
return service.signer(id, signataireId);
}
@POST
@Path("/{id}/archiver")
@RolesAllowed({"CONTROLEUR_INTERNE", "PRESIDENT", "SUPER_ADMIN"})
public RapportTrimestrielControleurInterne archiver(@PathParam("id") UUID id) {
return service.archiver(id);
}
@GET
@Path("/{id}/pdf")
@Produces("application/pdf")
@RolesAllowed({"CONTROLEUR_INTERNE", "PRESIDENT", "ADMIN_ORGANISATION", "SUPER_ADMIN"})
public Response telechargerPdf(@PathParam("id") UUID id) {
RapportTrimestrielControleurInterne r = repository.findById(id);
if (r == null || r.getPdfBytes() == null) {
throw new NotFoundException("Rapport ou PDF introuvable : " + id);
}
String filename = String.format("rapport-trim-%d-T%d.pdf", r.getAnnee(), r.getTrimestre());
return Response.ok(r.getPdfBytes())
.header("Content-Disposition", "attachment; filename=\"" + filename + "\"")
.build();
}
}