package dev.lions.unionflow.server.resource; import dev.lions.unionflow.server.api.payment.*; import dev.lions.unionflow.server.entity.SouscriptionOrganisation; import dev.lions.unionflow.server.payment.orchestration.PaymentOrchestrator; import dev.lions.unionflow.server.payment.orchestration.PaymentProviderRegistry; import dev.lions.unionflow.server.repository.SouscriptionOrganisationRepository; import jakarta.annotation.security.PermitAll; import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Inject; import jakarta.ws.rs.*; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import lombok.extern.slf4j.Slf4j; import java.math.BigDecimal; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.stream.Collectors; /** * Endpoints de paiement unifiés — abstraction multi-provider. * Remplace à terme les endpoints Wave-spécifiques. */ @Slf4j @Path("/api/paiements") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class PaiementUnifieResource { @Inject PaymentOrchestrator orchestrator; @Inject PaymentProviderRegistry registry; @Inject SouscriptionOrganisationRepository souscriptionRepository; /** * Initie un paiement via le provider demandé (ou le provider par défaut). * *

Exemple : {@code POST /api/paiements/initier?provider=WAVE} */ @POST @Path("/initier") @RolesAllowed({"MEMBRE_ACTIF", "ADMIN_ORGANISATION", "TRESORIER", "SUPER_ADMIN"}) public Response initier( @QueryParam("provider") String provider, PaiementInitierRequest req) { try { // Si une souscription est fournie, utiliser le providerDefaut de sa formule String resolvedProvider = provider; if (req.souscriptionId() != null) { resolvedProvider = souscriptionRepository.findByIdOptional(req.souscriptionId()) .map(SouscriptionOrganisation::getFormule) .map(f -> f.getProviderDefaut()) .filter(p -> p != null && !p.isBlank()) .orElse(provider); } CheckoutRequest checkoutRequest = new CheckoutRequest( req.montant(), req.devise() != null ? req.devise() : "XOF", req.telephone(), req.email(), req.reference(), req.successUrl(), req.cancelUrl(), Map.of() ); CheckoutSession session = orchestrator.initierPaiement(checkoutRequest, resolvedProvider); return Response.ok(session).build(); } catch (PaymentException e) { return Response.status(e.getHttpStatus()) .entity(Map.of("error", e.getMessage(), "provider", e.getProviderCode())) .build(); } } /** * Webhook entrant d'un provider. Vérifie la signature et met à jour le statut. * Route : {@code POST /api/paiements/webhook/{provider}} */ @POST @Path("/webhook/{provider}") @PermitAll @Consumes(MediaType.WILDCARD) public Response webhook( @PathParam("provider") String providerCode, String rawBody, @Context HttpHeaders httpHeaders) { try { PaymentProvider provider = registry.get(providerCode.toUpperCase()); Map headers = httpHeaders.getRequestHeaders().entrySet().stream() .collect(Collectors.toMap( Map.Entry::getKey, e -> e.getValue().isEmpty() ? "" : e.getValue().get(0) )); PaymentEvent event = provider.processWebhook(rawBody, headers); orchestrator.handleEvent(event); return Response.ok().build(); } catch (UnsupportedOperationException e) { return Response.status(Response.Status.NOT_FOUND) .entity(Map.of("error", "Provider inconnu : " + providerCode)) .build(); } catch (PaymentException e) { log.error("Webhook {} rejeté : {}", providerCode, e.getMessage()); return Response.status(e.getHttpStatus()) .entity(Map.of("error", e.getMessage())) .build(); } } /** Retourne les providers de paiement disponibles. */ @GET @Path("/providers") @RolesAllowed({"ADMIN_ORGANISATION", "SUPER_ADMIN"}) public List getProviders() { return registry.getAvailableCodes(); } public record PaiementInitierRequest( BigDecimal montant, String devise, String telephone, String email, String reference, String successUrl, String cancelUrl, /** Optionnel — si fourni, le providerDefaut de la formule prend le dessus sur le query param. */ UUID souscriptionId ) {} }