feat: HelloAsso payment

This commit is contained in:
2025-08-14 22:45:03 +02:00
parent 0a56f8c180
commit 15f65b1014
28 changed files with 1003 additions and 31 deletions

View File

@@ -0,0 +1,32 @@
package fr.titionfire.ffsaf.rest.client;
import fr.titionfire.ffsaf.rest.client.dto.TokenResponse;
import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
@Path("/")
@RegisterRestClient(configKey = "helloasso-auth")
public interface HelloAssoAuthClient {
@POST
@Path("/token")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.APPLICATION_JSON)
Uni<TokenResponse> getToken(
@FormParam("grant_type") String grantType,
@FormParam("client_id") String clientId,
@FormParam("client_secret") String clientSecret
);
@POST
@Path("/token")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.APPLICATION_JSON)
Uni<TokenResponse> refreshToken(
@FormParam("grant_type") String grantType,
@FormParam("client_id") String clientId,
@FormParam("refresh_token") String refreshToken
);
}

View File

@@ -0,0 +1,24 @@
package fr.titionfire.ffsaf.rest.client;
import fr.titionfire.ffsaf.domain.service.HelloAssoTokenService;
import io.quarkus.rest.client.reactive.ReactiveClientHeadersFactory;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.core.MultivaluedMap;
import org.jboss.resteasy.reactive.common.util.MultivaluedTreeMap;
@ApplicationScoped
public class HelloAssoHeadersFactory extends ReactiveClientHeadersFactory {
@Inject
HelloAssoTokenService helloAssoTokenService;
@Override
public Uni<MultivaluedMap<String, String>> getHeaders(MultivaluedMap<String, String> incomingHeaders,
MultivaluedMap<String, String> clientOutgoingHeaders) {
MultivaluedMap<String, String> map = new MultivaluedTreeMap<>();
return helloAssoTokenService.getValidAccessToken()
.invoke(token -> map.putSingle("Authorization", "Bearer " + token)).map(__ -> map);
}
}

View File

@@ -0,0 +1,51 @@
package fr.titionfire.ffsaf.rest.client;
import fr.titionfire.ffsaf.rest.client.dto.CheckoutIntentsRequest;
import fr.titionfire.ffsaf.rest.client.dto.CheckoutIntentsResponse;
import io.quarkus.rest.client.reactive.ClientExceptionMapper;
import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import java.io.ByteArrayInputStream;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
@Path("/")
@RegisterRestClient(configKey = "helloasso-api")
@RegisterClientHeaders(HelloAssoHeadersFactory.class)
public interface HelloAssoService {
@GET
@Path("/users/me/organizations")
@Produces("text/plain")
Uni<String> test();
@POST
@Path("organizations/{organizationSlug}/checkout-intents")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
Uni<CheckoutIntentsResponse> checkout(@PathParam("organizationSlug") String organizationSlug,
CheckoutIntentsRequest data);
@ClientExceptionMapper
static RuntimeException toException(Response response, Method method) {
if (!method.getDeclaringClass().getName().equals("fr.titionfire.ffsaf.rest.client.HelloAssoService"))
return null;
if (method.getName().equals("checkout")) {
if (response.getStatus() == 400) {
if (response.getEntity() instanceof ByteArrayInputStream) {
ByteArrayInputStream error = response.readEntity(ByteArrayInputStream.class);
return new RuntimeException(new String(error.readAllBytes(), StandardCharsets.UTF_8));
}
return new RuntimeException("The remote service responded with HTTP 400");
}
}
return null;
}
}

View File

@@ -0,0 +1,14 @@
package fr.titionfire.ffsaf.rest.client.dto;
import io.quarkus.runtime.annotations.RegisterForReflection;
@RegisterForReflection
public class ApiError {
public String error;
public String error_description;
@Override
public String toString() {
return error + ": " + error_description;
}
}

View File

@@ -0,0 +1,30 @@
package fr.titionfire.ffsaf.rest.client.dto;
import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@RegisterForReflection
public class CheckoutIntentsRequest {
public int totalAmount;
public int initialAmount;
public String itemName;
public String backUrl;
public String errorUrl;
public String returnUrl;
public boolean containsDonation;
public Payer payer;
public CheckoutMetadata metadata;
@Data
@AllArgsConstructor
@RegisterForReflection
public static class Payer {
public String firstName;
public String lastName;
public String email;
}
}

View File

@@ -0,0 +1,11 @@
package fr.titionfire.ffsaf.rest.client.dto;
import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.Data;
@Data
@RegisterForReflection
public class CheckoutIntentsResponse {
public int id;
public String redirectUrl;
}

View File

@@ -0,0 +1,12 @@
package fr.titionfire.ffsaf.rest.client.dto;
import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
@RegisterForReflection
public class CheckoutMetadata {
public long checkoutDBId;
}

View File

@@ -0,0 +1,16 @@
package fr.titionfire.ffsaf.rest.client.dto;
import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@RegisterForReflection
public class HelloassoNotification {
private NotificationData data;
private String eventType;
private CheckoutMetadata metadata;
}

View File

@@ -0,0 +1,30 @@
package fr.titionfire.ffsaf.rest.client.dto;
import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@RegisterForReflection
public class NotificationData {
private Order order;
private Integer id;
private String organizationSlug;
private String checkoutIntentId;
private String oldSlugOrganization; // Pour les changements de nom d'association
private String newSlugOrganization;
private String state; // Pour les formulaires
@Data
@NoArgsConstructor
@AllArgsConstructor
@RegisterForReflection
public static class Order {
private Integer id;
private String organizationSlug;
}
}

View File

@@ -0,0 +1,30 @@
package fr.titionfire.ffsaf.rest.client.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.quarkus.runtime.annotations.RegisterForReflection;
@RegisterForReflection
public class TokenResponse {
@JsonProperty("access_token")
public String accessToken;
@JsonProperty("refresh_token")
public String refreshToken;
@JsonProperty("token_type")
public String tokenType; // Toujours "bearer"
@JsonProperty("expires_in")
public long expiresIn; // Durée de validité en secondes (1800s = 30min)
// Pour stocker l'heure d'obtention du token
private long timestamp;
public TokenResponse() {
this.timestamp = System.currentTimeMillis() / 1000; // Timestamp en secondes
}
// Vérifie si le token est expiré
public boolean isExpired() {
return (System.currentTimeMillis() / 1000) - timestamp >= expiresIn;
}
}