mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-21 07:08:05 +01:00
Support SEPA
This commit is contained in:
@@ -455,12 +455,11 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
config.getAdminEventLoggingConfiguration().logName());
|
||||
|
||||
StripeManager stripeManager = new StripeManager(config.getStripe().apiKey().value(), subscriptionProcessorExecutor,
|
||||
config.getStripe().idempotencyKeyGenerator().value(), config.getStripe().boostDescription(), config.getStripe()
|
||||
.supportedCurrencies());
|
||||
config.getStripe().idempotencyKeyGenerator().value(), config.getStripe().boostDescription(), config.getStripe().supportedCurrenciesByPaymentMethod());
|
||||
BraintreeManager braintreeManager = new BraintreeManager(config.getBraintree().merchantId(),
|
||||
config.getBraintree().publicKey(), config.getBraintree().privateKey().value(),
|
||||
config.getBraintree().environment(),
|
||||
config.getBraintree().supportedCurrencies(), config.getBraintree().merchantAccounts(),
|
||||
config.getBraintree().supportedCurrenciesByPaymentMethod(), config.getBraintree().merchantAccounts(),
|
||||
config.getBraintree().graphqlUrl(), config.getBraintree().circuitBreaker(), subscriptionProcessorExecutor,
|
||||
subscriptionProcessorRetryExecutor);
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.PaymentMethod;
|
||||
|
||||
/**
|
||||
* @param merchantId the Braintree merchant ID
|
||||
@@ -27,7 +28,7 @@ public record BraintreeConfiguration(@NotBlank String merchantId,
|
||||
@NotBlank String publicKey,
|
||||
@NotNull SecretString privateKey,
|
||||
@NotBlank String environment,
|
||||
@NotEmpty Set<@NotBlank String> supportedCurrencies,
|
||||
@Valid @NotEmpty Map<PaymentMethod, Set<@NotBlank String>> supportedCurrenciesByPaymentMethod,
|
||||
@NotBlank String graphqlUrl,
|
||||
@NotEmpty Map<String, String> merchantAccounts,
|
||||
@NotNull
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import javax.validation.Valid;
|
||||
@@ -18,7 +19,8 @@ import javax.validation.constraints.Positive;
|
||||
*/
|
||||
public record OneTimeDonationConfiguration(@Valid ExpiringLevelConfiguration boost,
|
||||
@Valid ExpiringLevelConfiguration gift,
|
||||
Map<String, @Valid OneTimeDonationCurrencyConfiguration> currencies) {
|
||||
Map<String, @Valid OneTimeDonationCurrencyConfiguration> currencies,
|
||||
BigDecimal sepaMaxTransactionSizeEuros) {
|
||||
|
||||
/**
|
||||
* @param badge the numeric donation level ID
|
||||
|
||||
@@ -5,15 +5,18 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.configuration;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
|
||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.PaymentMethod;
|
||||
|
||||
public record StripeConfiguration(@NotNull SecretString apiKey,
|
||||
@NotNull SecretBytes idempotencyKeyGenerator,
|
||||
@NotBlank String boostDescription,
|
||||
@NotEmpty Set<@NotBlank String> supportedCurrencies) {
|
||||
@Valid @NotEmpty Map<PaymentMethod, Set<@NotBlank String>> supportedCurrenciesByPaymentMethod) {
|
||||
}
|
||||
|
||||
@@ -118,6 +118,7 @@ public class SubscriptionController {
|
||||
private static final String RECEIPT_ISSUED_COUNTER_NAME = MetricsUtil.name(SubscriptionController.class, "receiptIssued");
|
||||
private static final String PROCESSOR_TAG_NAME = "processor";
|
||||
private static final String TYPE_TAG_NAME = "type";
|
||||
private static final String EURO_CURRENCY_CODE = "EUR";
|
||||
|
||||
public SubscriptionController(
|
||||
@Nonnull Clock clock,
|
||||
@@ -170,8 +171,8 @@ public class SubscriptionController {
|
||||
|
||||
final List<String> supportedPaymentMethods = Arrays.stream(PaymentMethod.values())
|
||||
.filter(paymentMethod -> subscriptionProcessorManagers.stream()
|
||||
.anyMatch(manager -> manager.getSupportedCurrencies().contains(currency)
|
||||
&& manager.supportsPaymentMethod(paymentMethod)))
|
||||
.anyMatch(manager -> manager.supportsPaymentMethod(paymentMethod)
|
||||
&& manager.getSupportedCurrenciesForPaymentMethod(paymentMethod).contains(currency)))
|
||||
.map(PaymentMethod::name)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
@@ -377,7 +378,7 @@ public class SubscriptionController {
|
||||
|
||||
private SubscriptionProcessorManager getManagerForPaymentMethod(PaymentMethod paymentMethod) {
|
||||
return switch (paymentMethod) {
|
||||
case CARD -> stripeManager;
|
||||
case CARD, SEPA_DEBIT -> stripeManager;
|
||||
case PAYPAL -> braintreeManager;
|
||||
};
|
||||
}
|
||||
@@ -604,6 +605,7 @@ public class SubscriptionController {
|
||||
@Min(1)
|
||||
public long amount;
|
||||
public Long level;
|
||||
public PaymentMethod paymentMethod = PaymentMethod.CARD;
|
||||
}
|
||||
|
||||
public static class CreatePayPalBoostRequest extends CreateBoostRequest {
|
||||
@@ -651,15 +653,14 @@ public class SubscriptionController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the currency and amount in the request are supported by the {@code manager} and exceed the minimum
|
||||
* permitted amount
|
||||
* Validates that the currency is supported by the {@code manager} and {@code request.paymentMethod}
|
||||
* and that the amount meets minimum and maximum constraints.
|
||||
*
|
||||
* @throws BadRequestException indicates validation failed. Inspect {@code response.error} for details
|
||||
*/
|
||||
private void validateRequestCurrencyAmount(CreateBoostRequest request, BigDecimal amount,
|
||||
SubscriptionProcessorManager manager) {
|
||||
|
||||
if (!manager.supportsCurrency(request.currency.toLowerCase(Locale.ROOT))) {
|
||||
if (!manager.getSupportedCurrenciesForPaymentMethod(request.paymentMethod).contains(request.currency.toLowerCase(Locale.ROOT))) {
|
||||
throw new BadRequestException(Response.status(Status.BAD_REQUEST)
|
||||
.entity(Map.of("error", "unsupported_currency")).build());
|
||||
}
|
||||
@@ -675,6 +676,16 @@ public class SubscriptionController {
|
||||
"error", "amount_below_currency_minimum",
|
||||
"minimum", minCurrencyAmountMajorUnits.toString())).build());
|
||||
}
|
||||
|
||||
if (request.paymentMethod == PaymentMethod.SEPA_DEBIT &&
|
||||
amount.compareTo(SubscriptionCurrencyUtil.convertConfiguredAmountToApiAmount(
|
||||
EURO_CURRENCY_CODE,
|
||||
oneTimeDonationConfiguration.sepaMaxTransactionSizeEuros())) > 0) {
|
||||
throw new BadRequestException(Response.status(Status.BAD_REQUEST)
|
||||
.entity(Map.of(
|
||||
"error", "amount_above_sepa_limit",
|
||||
"maximum", oneTimeDonationConfiguration.sepaMaxTransactionSizeEuros().toString())).build());
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
|
||||
@@ -55,13 +55,13 @@ public class BraintreeManager implements SubscriptionProcessorManager {
|
||||
private final BraintreeGateway braintreeGateway;
|
||||
private final BraintreeGraphqlClient braintreeGraphqlClient;
|
||||
private final Executor executor;
|
||||
private final Set<String> supportedCurrencies;
|
||||
private final Map<PaymentMethod, Set<String>> supportedCurrenciesByPaymentMethod;
|
||||
private final Map<String, String> currenciesToMerchantAccounts;
|
||||
|
||||
public BraintreeManager(final String braintreeMerchantId, final String braintreePublicKey,
|
||||
final String braintreePrivateKey,
|
||||
final String braintreeEnvironment,
|
||||
final Set<String> supportedCurrencies,
|
||||
final Map<PaymentMethod, Set<String>> supportedCurrenciesByPaymentMethod,
|
||||
final Map<String, String> currenciesToMerchantAccounts,
|
||||
final String graphqlUri,
|
||||
final CircuitBreakerConfiguration circuitBreakerConfiguration,
|
||||
@@ -70,7 +70,7 @@ public class BraintreeManager implements SubscriptionProcessorManager {
|
||||
|
||||
this(new BraintreeGateway(braintreeEnvironment, braintreeMerchantId, braintreePublicKey,
|
||||
braintreePrivateKey),
|
||||
supportedCurrencies,
|
||||
supportedCurrenciesByPaymentMethod,
|
||||
currenciesToMerchantAccounts,
|
||||
new BraintreeGraphqlClient(FaultTolerantHttpClient.newBuilder()
|
||||
.withName("braintree-graphql")
|
||||
@@ -86,19 +86,20 @@ public class BraintreeManager implements SubscriptionProcessorManager {
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
BraintreeManager(final BraintreeGateway braintreeGateway, final Set<String> supportedCurrencies,
|
||||
BraintreeManager(final BraintreeGateway braintreeGateway,
|
||||
final Map<PaymentMethod, Set<String>> supportedCurrenciesByPaymentMethod,
|
||||
final Map<String, String> currenciesToMerchantAccounts, final BraintreeGraphqlClient braintreeGraphqlClient,
|
||||
final Executor executor) {
|
||||
this.braintreeGateway = braintreeGateway;
|
||||
this.supportedCurrencies = supportedCurrencies;
|
||||
this.supportedCurrenciesByPaymentMethod = supportedCurrenciesByPaymentMethod;
|
||||
this.currenciesToMerchantAccounts = currenciesToMerchantAccounts;
|
||||
this.braintreeGraphqlClient = braintreeGraphqlClient;
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getSupportedCurrencies() {
|
||||
return supportedCurrencies;
|
||||
public Set<String> getSupportedCurrenciesForPaymentMethod(final PaymentMethod paymentMethod) {
|
||||
return supportedCurrenciesByPaymentMethod.getOrDefault(paymentMethod, Collections.emptySet());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -111,11 +112,6 @@ public class BraintreeManager implements SubscriptionProcessorManager {
|
||||
return paymentMethod == PaymentMethod.PAYPAL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsCurrency(final String currency) {
|
||||
return supportedCurrencies.contains(currency.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<PaymentDetails> getPaymentDetails(final String paymentId) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
|
||||
@@ -14,4 +14,8 @@ public enum PaymentMethod {
|
||||
* A PayPal account
|
||||
*/
|
||||
PAYPAL,
|
||||
/**
|
||||
* A SEPA debit account
|
||||
*/
|
||||
SEPA_DEBIT,
|
||||
}
|
||||
|
||||
@@ -77,14 +77,14 @@ public class StripeManager implements SubscriptionProcessorManager {
|
||||
private final Executor executor;
|
||||
private final byte[] idempotencyKeyGenerator;
|
||||
private final String boostDescription;
|
||||
private final Set<String> supportedCurrencies;
|
||||
private final Map<PaymentMethod, Set<String>> supportedCurrenciesByPaymentMethod;
|
||||
|
||||
public StripeManager(
|
||||
@Nonnull String apiKey,
|
||||
@Nonnull Executor executor,
|
||||
@Nonnull byte[] idempotencyKeyGenerator,
|
||||
@Nonnull String boostDescription,
|
||||
@Nonnull Set<String> supportedCurrencies) {
|
||||
@Nonnull Map<PaymentMethod, Set<String>> supportedCurrenciesByPaymentMethod) {
|
||||
if (Strings.isNullOrEmpty(apiKey)) {
|
||||
throw new IllegalArgumentException("apiKey cannot be empty");
|
||||
}
|
||||
@@ -95,7 +95,7 @@ public class StripeManager implements SubscriptionProcessorManager {
|
||||
throw new IllegalArgumentException("idempotencyKeyGenerator cannot be empty");
|
||||
}
|
||||
this.boostDescription = Objects.requireNonNull(boostDescription);
|
||||
this.supportedCurrencies = supportedCurrencies;
|
||||
this.supportedCurrenciesByPaymentMethod = supportedCurrenciesByPaymentMethod;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -105,12 +105,7 @@ public class StripeManager implements SubscriptionProcessorManager {
|
||||
|
||||
@Override
|
||||
public boolean supportsPaymentMethod(PaymentMethod paymentMethod) {
|
||||
return paymentMethod == PaymentMethod.CARD;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsCurrency(final String currency) {
|
||||
return supportedCurrencies.contains(currency);
|
||||
return paymentMethod == PaymentMethod.CARD || paymentMethod == PaymentMethod.SEPA_DEBIT;
|
||||
}
|
||||
|
||||
private RequestOptions commonOptions() {
|
||||
@@ -184,8 +179,8 @@ public class StripeManager implements SubscriptionProcessorManager {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getSupportedCurrencies() {
|
||||
return supportedCurrencies;
|
||||
public Set<String> getSupportedCurrenciesForPaymentMethod(final PaymentMethod paymentMethod) {
|
||||
return supportedCurrenciesByPaymentMethod.getOrDefault(paymentMethod, Collections.emptySet());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,9 +19,7 @@ public interface SubscriptionProcessorManager {
|
||||
|
||||
boolean supportsPaymentMethod(PaymentMethod paymentMethod);
|
||||
|
||||
boolean supportsCurrency(String currency);
|
||||
|
||||
Set<String> getSupportedCurrencies();
|
||||
Set<String> getSupportedCurrenciesForPaymentMethod(PaymentMethod paymentMethod);
|
||||
|
||||
CompletableFuture<PaymentDetails> getPaymentDetails(String paymentId);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user