feat(versement): nouveau module Versement (paiements rattachés à des objets)

- Entités : Versement, VersementObjet (lien polymorphique vers cotisation/adhesion/etc.)
- VersementRepository : requêtes par membre, org, période
- VersementResource : endpoints REST /api/versements
- VersementService : logique métier (validation, rattachement objets)
- Migration V27 : ajout numeroTelephone sur versements
This commit is contained in:
dahoud
2026-04-15 20:23:17 +00:00
parent 719d45e1fe
commit 5d028a10bf
6 changed files with 1070 additions and 0 deletions

View File

@@ -0,0 +1,170 @@
package dev.lions.unionflow.server.resource;
import dev.lions.unionflow.server.api.dto.versement.request.DeclarerVersementManuelRequest;
import dev.lions.unionflow.server.api.dto.versement.request.InitierDepotEpargneRequest;
import dev.lions.unionflow.server.api.dto.versement.request.InitierVersementWaveRequest;
import dev.lions.unionflow.server.api.dto.versement.response.VersementGatewayResponse;
import dev.lions.unionflow.server.api.dto.versement.response.VersementResponse;
import dev.lions.unionflow.server.api.dto.versement.response.VersementStatutResponse;
import dev.lions.unionflow.server.api.dto.versement.response.VersementSummaryResponse;
import dev.lions.unionflow.server.service.VersementService;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.List;
import java.util.UUID;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.jboss.logging.Logger;
/**
* Resource REST pour la gestion des versements.
*
* <p>Endpoints principaux :
* <ul>
* <li>{@code POST /api/versements/initier-wave} — démarre le flux Wave deep link</li>
* <li>{@code GET /api/versements/statut/{intentionId}} — retour deep link / polling</li>
* <li>{@code POST /api/versements/declarer-manuel} — déclaration espèces/virement/chèque</li>
* <li>{@code GET /api/versements/mes-versements} — historique du membre connecté</li>
* </ul>
*
* @author UnionFlow Team
* @version 4.0
* @since 2026-04-13
*/
@Path("/api/versements")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION", "MEMBRE", "USER"})
@Tag(name = "Versements", description = "Versements de cotisations — Wave et manuel")
public class VersementResource {
private static final Logger LOG = Logger.getLogger(VersementResource.class);
@Inject
VersementService versementService;
// ── Lecture ───────────────────────────────────────────────────────────────
@GET
@Path("/{id}")
public Response trouverParId(@PathParam("id") UUID id) {
LOG.infof("GET /api/versements/%s", id);
VersementResponse result = versementService.trouverParId(id);
return Response.ok(result).build();
}
@GET
@Path("/reference/{numeroReference}")
public Response trouverParNumeroReference(
@PathParam("numeroReference") String numeroReference) {
LOG.infof("GET /api/versements/reference/%s", numeroReference);
VersementResponse result = versementService.trouverParNumeroReference(numeroReference);
return Response.ok(result).build();
}
@GET
@Path("/membre/{membreId}")
public Response listerParMembre(@PathParam("membreId") UUID membreId) {
LOG.infof("GET /api/versements/membre/%s", membreId);
List<VersementSummaryResponse> result = versementService.listerParMembre(membreId);
return Response.ok(result).build();
}
@GET
@Path("/mes-versements")
@RolesAllowed({"MEMBRE", "ADMIN", "ADMIN_ORGANISATION"})
public Response getMesVersements(
@QueryParam("limit") @DefaultValue("20") int limit) {
LOG.infof("GET /api/versements/mes-versements?limit=%d", limit);
List<VersementSummaryResponse> result = versementService.getMesVersements(limit);
return Response.ok(result).build();
}
// ── Validation / Annulation ───────────────────────────────────────────────
@POST
@Path("/{id}/valider")
@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION"})
public Response validerVersement(@PathParam("id") UUID id) {
LOG.infof("POST /api/versements/%s/valider", id);
VersementResponse result = versementService.validerVersement(id);
return Response.ok(result).build();
}
@POST
@Path("/{id}/annuler")
@RolesAllowed({"ADMIN", "ADMIN_ORGANISATION", "MEMBRE"})
public Response annulerVersement(@PathParam("id") UUID id) {
LOG.infof("POST /api/versements/%s/annuler", id);
VersementResponse result = versementService.annulerVersement(id);
return Response.ok(result).build();
}
// ── Flux Wave ─────────────────────────────────────────────────────────────
/**
* Initie un versement Wave.
*
* <p>Le mobile appelle cet endpoint puis ouvre le {@code waveLaunchUrl} retourné
* avec {@code url_launcher}. Wave s'ouvre avec le montant et le numéro pré-remplis.
* Après confirmation, Wave redirige vers
* {@code unionflow://payment?result=success&ref={clientReference}}.
*/
@POST
@Path("/initier-wave")
@RolesAllowed({"MEMBRE", "MEMBRE_ACTIF", "ADMIN", "ADMIN_ORGANISATION", "USER"})
public Response initierVersementWave(@Valid InitierVersementWaveRequest request) {
LOG.infof("POST /api/versements/initier-wave — cotisation: %s", request.cotisationId());
VersementGatewayResponse result = versementService.initierVersementWave(request);
return Response.status(Response.Status.CREATED).entity(result).build();
}
/**
* Retour deep link / polling du statut d'un versement Wave.
*
* <p>Appelé par le mobile au retour du deep link
* {@code unionflow://payment?result=success&ref={intentionId}}
* pour confirmer que le paiement est bien enregistré côté UnionFlow.
* Également utilisé par le web en polling toutes les 3 secondes.
*/
@GET
@Path("/statut/{intentionId}")
@RolesAllowed({"MEMBRE", "MEMBRE_ACTIF", "ADMIN", "ADMIN_ORGANISATION", "USER"})
public Response getStatutVersement(@PathParam("intentionId") UUID intentionId) {
LOG.infof("GET /api/versements/statut/%s", intentionId);
VersementStatutResponse result = versementService.verifierStatutVersement(intentionId);
return Response.ok(result).build();
}
// ── Flux manuel ───────────────────────────────────────────────────────────
/**
* Déclare un versement manuel (espèces, virement, chèque).
* Le versement est créé avec le statut EN_ATTENTE_VALIDATION.
* Le trésorier devra le valider via la page admin.
*/
@POST
@Path("/declarer-manuel")
@RolesAllowed({"MEMBRE", "ADMIN", "ADMIN_ORGANISATION"})
public Response declarerVersementManuel(@Valid DeclarerVersementManuelRequest request) {
LOG.infof("POST /api/versements/declarer-manuel — cotisation: %s, méthode: %s",
request.cotisationId(), request.methodePaiement());
VersementResponse result = versementService.declarerVersementManuel(request);
return Response.status(Response.Status.CREATED).entity(result).build();
}
// ── Dépôt épargne ─────────────────────────────────────────────────────────
@POST
@Path("/initier-depot-epargne")
@RolesAllowed({"MEMBRE", "MEMBRE_ACTIF", "ADMIN", "ADMIN_ORGANISATION", "USER"})
public Response initierDepotEpargne(@Valid InitierDepotEpargneRequest request) {
LOG.infof("POST /api/versements/initier-depot-epargne — compte: %s, montant: %s",
request.compteId(), request.montant());
VersementGatewayResponse result = versementService.initierDepotEpargneEnLigne(request);
return Response.status(Response.Status.CREATED).entity(result).build();
}
}