Support SEPA
This commit is contained in:
parent
9cd21d1326
commit
a00c2fcfdb
|
@ -55,10 +55,12 @@ stripe:
|
||||||
idempotencyKeyGenerator: secret://stripe.idempotencyKeyGenerator
|
idempotencyKeyGenerator: secret://stripe.idempotencyKeyGenerator
|
||||||
boostDescription: >
|
boostDescription: >
|
||||||
Example
|
Example
|
||||||
supportedCurrencies:
|
supportedCurrenciesByPaymentMethod:
|
||||||
- xts
|
CARD:
|
||||||
# - ...
|
- usd
|
||||||
# - Nth supported currency
|
- eur
|
||||||
|
SEPA_DEBIT:
|
||||||
|
- eur
|
||||||
|
|
||||||
|
|
||||||
braintree:
|
braintree:
|
||||||
|
@ -70,10 +72,9 @@ braintree:
|
||||||
merchantAccounts:
|
merchantAccounts:
|
||||||
# ISO 4217 currency code and its corresponding sub-merchant account
|
# ISO 4217 currency code and its corresponding sub-merchant account
|
||||||
'xts': unset
|
'xts': unset
|
||||||
supportedCurrencies:
|
supportedCurrenciesByPaymentMethod:
|
||||||
- xts
|
PAYPAL:
|
||||||
# - ...
|
- usd
|
||||||
# - Nth supported currency
|
|
||||||
|
|
||||||
dynamoDbClientConfiguration:
|
dynamoDbClientConfiguration:
|
||||||
region: us-west-2 # AWS Region
|
region: us-west-2 # AWS Region
|
||||||
|
@ -367,6 +368,7 @@ subscription: # configuration for Stripe subscriptions
|
||||||
BRAINTREE: plan_example # braintree Plan ID
|
BRAINTREE: plan_example # braintree Plan ID
|
||||||
|
|
||||||
oneTimeDonations:
|
oneTimeDonations:
|
||||||
|
sepaMaxTransactionSizeEuros: '10000'
|
||||||
boost:
|
boost:
|
||||||
level: 1
|
level: 1
|
||||||
expiration: P90D
|
expiration: P90D
|
||||||
|
|
|
@ -455,12 +455,11 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
config.getAdminEventLoggingConfiguration().logName());
|
config.getAdminEventLoggingConfiguration().logName());
|
||||||
|
|
||||||
StripeManager stripeManager = new StripeManager(config.getStripe().apiKey().value(), subscriptionProcessorExecutor,
|
StripeManager stripeManager = new StripeManager(config.getStripe().apiKey().value(), subscriptionProcessorExecutor,
|
||||||
config.getStripe().idempotencyKeyGenerator().value(), config.getStripe().boostDescription(), config.getStripe()
|
config.getStripe().idempotencyKeyGenerator().value(), config.getStripe().boostDescription(), config.getStripe().supportedCurrenciesByPaymentMethod());
|
||||||
.supportedCurrencies());
|
|
||||||
BraintreeManager braintreeManager = new BraintreeManager(config.getBraintree().merchantId(),
|
BraintreeManager braintreeManager = new BraintreeManager(config.getBraintree().merchantId(),
|
||||||
config.getBraintree().publicKey(), config.getBraintree().privateKey().value(),
|
config.getBraintree().publicKey(), config.getBraintree().privateKey().value(),
|
||||||
config.getBraintree().environment(),
|
config.getBraintree().environment(),
|
||||||
config.getBraintree().supportedCurrencies(), config.getBraintree().merchantAccounts(),
|
config.getBraintree().supportedCurrenciesByPaymentMethod(), config.getBraintree().merchantAccounts(),
|
||||||
config.getBraintree().graphqlUrl(), config.getBraintree().circuitBreaker(), subscriptionProcessorExecutor,
|
config.getBraintree().graphqlUrl(), config.getBraintree().circuitBreaker(), subscriptionProcessorExecutor,
|
||||||
subscriptionProcessorRetryExecutor);
|
subscriptionProcessorRetryExecutor);
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import javax.validation.constraints.NotBlank;
|
||||||
import javax.validation.constraints.NotEmpty;
|
import javax.validation.constraints.NotEmpty;
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
|
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
|
||||||
|
import org.whispersystems.textsecuregcm.subscriptions.PaymentMethod;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param merchantId the Braintree merchant ID
|
* @param merchantId the Braintree merchant ID
|
||||||
|
@ -27,7 +28,7 @@ public record BraintreeConfiguration(@NotBlank String merchantId,
|
||||||
@NotBlank String publicKey,
|
@NotBlank String publicKey,
|
||||||
@NotNull SecretString privateKey,
|
@NotNull SecretString privateKey,
|
||||||
@NotBlank String environment,
|
@NotBlank String environment,
|
||||||
@NotEmpty Set<@NotBlank String> supportedCurrencies,
|
@Valid @NotEmpty Map<PaymentMethod, Set<@NotBlank String>> supportedCurrenciesByPaymentMethod,
|
||||||
@NotBlank String graphqlUrl,
|
@NotBlank String graphqlUrl,
|
||||||
@NotEmpty Map<String, String> merchantAccounts,
|
@NotEmpty Map<String, String> merchantAccounts,
|
||||||
@NotNull
|
@NotNull
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.configuration;
|
package org.whispersystems.textsecuregcm.configuration;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
|
@ -18,7 +19,8 @@ import javax.validation.constraints.Positive;
|
||||||
*/
|
*/
|
||||||
public record OneTimeDonationConfiguration(@Valid ExpiringLevelConfiguration boost,
|
public record OneTimeDonationConfiguration(@Valid ExpiringLevelConfiguration boost,
|
||||||
@Valid ExpiringLevelConfiguration gift,
|
@Valid ExpiringLevelConfiguration gift,
|
||||||
Map<String, @Valid OneTimeDonationCurrencyConfiguration> currencies) {
|
Map<String, @Valid OneTimeDonationCurrencyConfiguration> currencies,
|
||||||
|
BigDecimal sepaMaxTransactionSizeEuros) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param badge the numeric donation level ID
|
* @param badge the numeric donation level ID
|
||||||
|
|
|
@ -5,15 +5,18 @@
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.configuration;
|
package org.whispersystems.textsecuregcm.configuration;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import javax.validation.Valid;
|
||||||
import javax.validation.constraints.NotBlank;
|
import javax.validation.constraints.NotBlank;
|
||||||
import javax.validation.constraints.NotEmpty;
|
import javax.validation.constraints.NotEmpty;
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
|
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
|
||||||
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
|
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
|
||||||
|
import org.whispersystems.textsecuregcm.subscriptions.PaymentMethod;
|
||||||
|
|
||||||
public record StripeConfiguration(@NotNull SecretString apiKey,
|
public record StripeConfiguration(@NotNull SecretString apiKey,
|
||||||
@NotNull SecretBytes idempotencyKeyGenerator,
|
@NotNull SecretBytes idempotencyKeyGenerator,
|
||||||
@NotBlank String boostDescription,
|
@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 RECEIPT_ISSUED_COUNTER_NAME = MetricsUtil.name(SubscriptionController.class, "receiptIssued");
|
||||||
private static final String PROCESSOR_TAG_NAME = "processor";
|
private static final String PROCESSOR_TAG_NAME = "processor";
|
||||||
private static final String TYPE_TAG_NAME = "type";
|
private static final String TYPE_TAG_NAME = "type";
|
||||||
|
private static final String EURO_CURRENCY_CODE = "EUR";
|
||||||
|
|
||||||
public SubscriptionController(
|
public SubscriptionController(
|
||||||
@Nonnull Clock clock,
|
@Nonnull Clock clock,
|
||||||
|
@ -170,8 +171,8 @@ public class SubscriptionController {
|
||||||
|
|
||||||
final List<String> supportedPaymentMethods = Arrays.stream(PaymentMethod.values())
|
final List<String> supportedPaymentMethods = Arrays.stream(PaymentMethod.values())
|
||||||
.filter(paymentMethod -> subscriptionProcessorManagers.stream()
|
.filter(paymentMethod -> subscriptionProcessorManagers.stream()
|
||||||
.anyMatch(manager -> manager.getSupportedCurrencies().contains(currency)
|
.anyMatch(manager -> manager.supportsPaymentMethod(paymentMethod)
|
||||||
&& manager.supportsPaymentMethod(paymentMethod)))
|
&& manager.getSupportedCurrenciesForPaymentMethod(paymentMethod).contains(currency)))
|
||||||
.map(PaymentMethod::name)
|
.map(PaymentMethod::name)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
@ -377,7 +378,7 @@ public class SubscriptionController {
|
||||||
|
|
||||||
private SubscriptionProcessorManager getManagerForPaymentMethod(PaymentMethod paymentMethod) {
|
private SubscriptionProcessorManager getManagerForPaymentMethod(PaymentMethod paymentMethod) {
|
||||||
return switch (paymentMethod) {
|
return switch (paymentMethod) {
|
||||||
case CARD -> stripeManager;
|
case CARD, SEPA_DEBIT -> stripeManager;
|
||||||
case PAYPAL -> braintreeManager;
|
case PAYPAL -> braintreeManager;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -604,6 +605,7 @@ public class SubscriptionController {
|
||||||
@Min(1)
|
@Min(1)
|
||||||
public long amount;
|
public long amount;
|
||||||
public Long level;
|
public Long level;
|
||||||
|
public PaymentMethod paymentMethod = PaymentMethod.CARD;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class CreatePayPalBoostRequest extends CreateBoostRequest {
|
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
|
* Validates that the currency is supported by the {@code manager} and {@code request.paymentMethod}
|
||||||
* permitted amount
|
* and that the amount meets minimum and maximum constraints.
|
||||||
*
|
*
|
||||||
* @throws BadRequestException indicates validation failed. Inspect {@code response.error} for details
|
* @throws BadRequestException indicates validation failed. Inspect {@code response.error} for details
|
||||||
*/
|
*/
|
||||||
private void validateRequestCurrencyAmount(CreateBoostRequest request, BigDecimal amount,
|
private void validateRequestCurrencyAmount(CreateBoostRequest request, BigDecimal amount,
|
||||||
SubscriptionProcessorManager manager) {
|
SubscriptionProcessorManager manager) {
|
||||||
|
if (!manager.getSupportedCurrenciesForPaymentMethod(request.paymentMethod).contains(request.currency.toLowerCase(Locale.ROOT))) {
|
||||||
if (!manager.supportsCurrency(request.currency.toLowerCase(Locale.ROOT))) {
|
|
||||||
throw new BadRequestException(Response.status(Status.BAD_REQUEST)
|
throw new BadRequestException(Response.status(Status.BAD_REQUEST)
|
||||||
.entity(Map.of("error", "unsupported_currency")).build());
|
.entity(Map.of("error", "unsupported_currency")).build());
|
||||||
}
|
}
|
||||||
|
@ -675,6 +676,16 @@ public class SubscriptionController {
|
||||||
"error", "amount_below_currency_minimum",
|
"error", "amount_below_currency_minimum",
|
||||||
"minimum", minCurrencyAmountMajorUnits.toString())).build());
|
"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
|
@POST
|
||||||
|
|
|
@ -55,13 +55,13 @@ public class BraintreeManager implements SubscriptionProcessorManager {
|
||||||
private final BraintreeGateway braintreeGateway;
|
private final BraintreeGateway braintreeGateway;
|
||||||
private final BraintreeGraphqlClient braintreeGraphqlClient;
|
private final BraintreeGraphqlClient braintreeGraphqlClient;
|
||||||
private final Executor executor;
|
private final Executor executor;
|
||||||
private final Set<String> supportedCurrencies;
|
private final Map<PaymentMethod, Set<String>> supportedCurrenciesByPaymentMethod;
|
||||||
private final Map<String, String> currenciesToMerchantAccounts;
|
private final Map<String, String> currenciesToMerchantAccounts;
|
||||||
|
|
||||||
public BraintreeManager(final String braintreeMerchantId, final String braintreePublicKey,
|
public BraintreeManager(final String braintreeMerchantId, final String braintreePublicKey,
|
||||||
final String braintreePrivateKey,
|
final String braintreePrivateKey,
|
||||||
final String braintreeEnvironment,
|
final String braintreeEnvironment,
|
||||||
final Set<String> supportedCurrencies,
|
final Map<PaymentMethod, Set<String>> supportedCurrenciesByPaymentMethod,
|
||||||
final Map<String, String> currenciesToMerchantAccounts,
|
final Map<String, String> currenciesToMerchantAccounts,
|
||||||
final String graphqlUri,
|
final String graphqlUri,
|
||||||
final CircuitBreakerConfiguration circuitBreakerConfiguration,
|
final CircuitBreakerConfiguration circuitBreakerConfiguration,
|
||||||
|
@ -70,7 +70,7 @@ public class BraintreeManager implements SubscriptionProcessorManager {
|
||||||
|
|
||||||
this(new BraintreeGateway(braintreeEnvironment, braintreeMerchantId, braintreePublicKey,
|
this(new BraintreeGateway(braintreeEnvironment, braintreeMerchantId, braintreePublicKey,
|
||||||
braintreePrivateKey),
|
braintreePrivateKey),
|
||||||
supportedCurrencies,
|
supportedCurrenciesByPaymentMethod,
|
||||||
currenciesToMerchantAccounts,
|
currenciesToMerchantAccounts,
|
||||||
new BraintreeGraphqlClient(FaultTolerantHttpClient.newBuilder()
|
new BraintreeGraphqlClient(FaultTolerantHttpClient.newBuilder()
|
||||||
.withName("braintree-graphql")
|
.withName("braintree-graphql")
|
||||||
|
@ -86,19 +86,20 @@ public class BraintreeManager implements SubscriptionProcessorManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@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 Map<String, String> currenciesToMerchantAccounts, final BraintreeGraphqlClient braintreeGraphqlClient,
|
||||||
final Executor executor) {
|
final Executor executor) {
|
||||||
this.braintreeGateway = braintreeGateway;
|
this.braintreeGateway = braintreeGateway;
|
||||||
this.supportedCurrencies = supportedCurrencies;
|
this.supportedCurrenciesByPaymentMethod = supportedCurrenciesByPaymentMethod;
|
||||||
this.currenciesToMerchantAccounts = currenciesToMerchantAccounts;
|
this.currenciesToMerchantAccounts = currenciesToMerchantAccounts;
|
||||||
this.braintreeGraphqlClient = braintreeGraphqlClient;
|
this.braintreeGraphqlClient = braintreeGraphqlClient;
|
||||||
this.executor = executor;
|
this.executor = executor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Set<String> getSupportedCurrencies() {
|
public Set<String> getSupportedCurrenciesForPaymentMethod(final PaymentMethod paymentMethod) {
|
||||||
return supportedCurrencies;
|
return supportedCurrenciesByPaymentMethod.getOrDefault(paymentMethod, Collections.emptySet());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -111,11 +112,6 @@ public class BraintreeManager implements SubscriptionProcessorManager {
|
||||||
return paymentMethod == PaymentMethod.PAYPAL;
|
return paymentMethod == PaymentMethod.PAYPAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean supportsCurrency(final String currency) {
|
|
||||||
return supportedCurrencies.contains(currency.toLowerCase(Locale.ROOT));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CompletableFuture<PaymentDetails> getPaymentDetails(final String paymentId) {
|
public CompletableFuture<PaymentDetails> getPaymentDetails(final String paymentId) {
|
||||||
return CompletableFuture.supplyAsync(() -> {
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
|
|
@ -14,4 +14,8 @@ public enum PaymentMethod {
|
||||||
* A PayPal account
|
* A PayPal account
|
||||||
*/
|
*/
|
||||||
PAYPAL,
|
PAYPAL,
|
||||||
|
/**
|
||||||
|
* A SEPA debit account
|
||||||
|
*/
|
||||||
|
SEPA_DEBIT,
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,14 +77,14 @@ public class StripeManager implements SubscriptionProcessorManager {
|
||||||
private final Executor executor;
|
private final Executor executor;
|
||||||
private final byte[] idempotencyKeyGenerator;
|
private final byte[] idempotencyKeyGenerator;
|
||||||
private final String boostDescription;
|
private final String boostDescription;
|
||||||
private final Set<String> supportedCurrencies;
|
private final Map<PaymentMethod, Set<String>> supportedCurrenciesByPaymentMethod;
|
||||||
|
|
||||||
public StripeManager(
|
public StripeManager(
|
||||||
@Nonnull String apiKey,
|
@Nonnull String apiKey,
|
||||||
@Nonnull Executor executor,
|
@Nonnull Executor executor,
|
||||||
@Nonnull byte[] idempotencyKeyGenerator,
|
@Nonnull byte[] idempotencyKeyGenerator,
|
||||||
@Nonnull String boostDescription,
|
@Nonnull String boostDescription,
|
||||||
@Nonnull Set<String> supportedCurrencies) {
|
@Nonnull Map<PaymentMethod, Set<String>> supportedCurrenciesByPaymentMethod) {
|
||||||
if (Strings.isNullOrEmpty(apiKey)) {
|
if (Strings.isNullOrEmpty(apiKey)) {
|
||||||
throw new IllegalArgumentException("apiKey cannot be empty");
|
throw new IllegalArgumentException("apiKey cannot be empty");
|
||||||
}
|
}
|
||||||
|
@ -95,7 +95,7 @@ public class StripeManager implements SubscriptionProcessorManager {
|
||||||
throw new IllegalArgumentException("idempotencyKeyGenerator cannot be empty");
|
throw new IllegalArgumentException("idempotencyKeyGenerator cannot be empty");
|
||||||
}
|
}
|
||||||
this.boostDescription = Objects.requireNonNull(boostDescription);
|
this.boostDescription = Objects.requireNonNull(boostDescription);
|
||||||
this.supportedCurrencies = supportedCurrencies;
|
this.supportedCurrenciesByPaymentMethod = supportedCurrenciesByPaymentMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -105,12 +105,7 @@ public class StripeManager implements SubscriptionProcessorManager {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean supportsPaymentMethod(PaymentMethod paymentMethod) {
|
public boolean supportsPaymentMethod(PaymentMethod paymentMethod) {
|
||||||
return paymentMethod == PaymentMethod.CARD;
|
return paymentMethod == PaymentMethod.CARD || paymentMethod == PaymentMethod.SEPA_DEBIT;
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean supportsCurrency(final String currency) {
|
|
||||||
return supportedCurrencies.contains(currency);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private RequestOptions commonOptions() {
|
private RequestOptions commonOptions() {
|
||||||
|
@ -184,8 +179,8 @@ public class StripeManager implements SubscriptionProcessorManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Set<String> getSupportedCurrencies() {
|
public Set<String> getSupportedCurrenciesForPaymentMethod(final PaymentMethod paymentMethod) {
|
||||||
return supportedCurrencies;
|
return supportedCurrenciesByPaymentMethod.getOrDefault(paymentMethod, Collections.emptySet());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -19,9 +19,7 @@ public interface SubscriptionProcessorManager {
|
||||||
|
|
||||||
boolean supportsPaymentMethod(PaymentMethod paymentMethod);
|
boolean supportsPaymentMethod(PaymentMethod paymentMethod);
|
||||||
|
|
||||||
boolean supportsCurrency(String currency);
|
Set<String> getSupportedCurrenciesForPaymentMethod(PaymentMethod paymentMethod);
|
||||||
|
|
||||||
Set<String> getSupportedCurrencies();
|
|
||||||
|
|
||||||
CompletableFuture<PaymentDetails> getPaymentDetails(String paymentId);
|
CompletableFuture<PaymentDetails> getPaymentDetails(String paymentId);
|
||||||
|
|
||||||
|
|
|
@ -68,6 +68,7 @@ import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.SubscriptionManager;
|
import org.whispersystems.textsecuregcm.storage.SubscriptionManager;
|
||||||
import org.whispersystems.textsecuregcm.subscriptions.BraintreeManager;
|
import org.whispersystems.textsecuregcm.subscriptions.BraintreeManager;
|
||||||
import org.whispersystems.textsecuregcm.subscriptions.ChargeFailure;
|
import org.whispersystems.textsecuregcm.subscriptions.ChargeFailure;
|
||||||
|
import org.whispersystems.textsecuregcm.subscriptions.PaymentMethod;
|
||||||
import org.whispersystems.textsecuregcm.subscriptions.ProcessorCustomer;
|
import org.whispersystems.textsecuregcm.subscriptions.ProcessorCustomer;
|
||||||
import org.whispersystems.textsecuregcm.subscriptions.StripeManager;
|
import org.whispersystems.textsecuregcm.subscriptions.StripeManager;
|
||||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessor;
|
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessor;
|
||||||
|
@ -98,9 +99,11 @@ class SubscriptionControllerTest {
|
||||||
when(manager.supportsPaymentMethod(any()))
|
when(manager.supportsPaymentMethod(any()))
|
||||||
.thenCallRealMethod();
|
.thenCallRealMethod();
|
||||||
});
|
});
|
||||||
when(STRIPE_MANAGER.getSupportedCurrencies())
|
when(STRIPE_MANAGER.getSupportedCurrenciesForPaymentMethod(PaymentMethod.CARD))
|
||||||
.thenReturn(Set.of("usd", "jpy", "bif"));
|
.thenReturn(Set.of("usd", "jpy", "bif", "eur"));
|
||||||
when(BRAINTREE_MANAGER.getSupportedCurrencies())
|
when(STRIPE_MANAGER.getSupportedCurrenciesForPaymentMethod(PaymentMethod.SEPA_DEBIT))
|
||||||
|
.thenReturn(Set.of("eur"));
|
||||||
|
when(BRAINTREE_MANAGER.getSupportedCurrenciesForPaymentMethod(PaymentMethod.PAYPAL))
|
||||||
.thenReturn(Set.of("usd", "jpy"));
|
.thenReturn(Set.of("usd", "jpy"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,7 +137,8 @@ class SubscriptionControllerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCreateBoostPaymentIntentAmountBelowCurrencyMinimum() {
|
void testCreateBoostPaymentIntentAmountBelowCurrencyMinimum() {
|
||||||
when(STRIPE_MANAGER.supportsCurrency("usd")).thenReturn(true);
|
when(STRIPE_MANAGER.getSupportedCurrenciesForPaymentMethod(PaymentMethod.CARD))
|
||||||
|
.thenReturn(Set.of("usd", "jpy", "bif", "eur"));
|
||||||
final Response response = RESOURCE_EXTENSION.target("/v1/subscription/boost/create")
|
final Response response = RESOURCE_EXTENSION.target("/v1/subscription/boost/create")
|
||||||
.request()
|
.request()
|
||||||
.post(Entity.json("""
|
.post(Entity.json("""
|
||||||
|
@ -146,16 +150,58 @@ class SubscriptionControllerTest {
|
||||||
"""));
|
"""));
|
||||||
assertThat(response.getStatus()).isEqualTo(400);
|
assertThat(response.getStatus()).isEqualTo(400);
|
||||||
assertThat(response.hasEntity()).isTrue();
|
assertThat(response.hasEntity()).isTrue();
|
||||||
assertThat(response.readEntity(Map.class))
|
final Map responseMap = response.readEntity(Map.class);
|
||||||
.isNotNull()
|
assertThat(responseMap.get("error")).isEqualTo("amount_below_currency_minimum");
|
||||||
.containsAllEntriesOf(Map.of(
|
assertThat(responseMap.get("minimum")).isEqualTo("2.50");
|
||||||
"error", "amount_below_currency_minimum",
|
}
|
||||||
"minimum", "2.50"
|
|
||||||
));
|
@Test
|
||||||
|
void testCreateBoostPaymentIntentAmountAboveSepaLimit() {
|
||||||
|
when(STRIPE_MANAGER.getSupportedCurrenciesForPaymentMethod(PaymentMethod.SEPA_DEBIT))
|
||||||
|
.thenReturn(Set.of("eur"));
|
||||||
|
final Response response = RESOURCE_EXTENSION.target("/v1/subscription/boost/create")
|
||||||
|
.request()
|
||||||
|
.post(Entity.json("""
|
||||||
|
{
|
||||||
|
"currency": "EUR",
|
||||||
|
"amount": 1000001,
|
||||||
|
"level": null,
|
||||||
|
"paymentMethod": "SEPA_DEBIT"
|
||||||
|
}
|
||||||
|
"""));
|
||||||
|
assertThat(response.getStatus()).isEqualTo(400);
|
||||||
|
assertThat(response.hasEntity()).isTrue();
|
||||||
|
|
||||||
|
final Map responseMap = response.readEntity(Map.class);
|
||||||
|
assertThat(responseMap.get("error")).isEqualTo("amount_above_sepa_limit");
|
||||||
|
assertThat(responseMap.get("maximum")).isEqualTo("10000");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateBoostPaymentIntentUnsupportedCurrency() {
|
||||||
|
when(STRIPE_MANAGER.getSupportedCurrenciesForPaymentMethod(PaymentMethod.SEPA_DEBIT))
|
||||||
|
.thenReturn(Set.of("eur"));
|
||||||
|
final Response response = RESOURCE_EXTENSION.target("/v1/subscription/boost/create")
|
||||||
|
.request()
|
||||||
|
.post(Entity.json("""
|
||||||
|
{
|
||||||
|
"currency": "USD",
|
||||||
|
"amount": 3000,
|
||||||
|
"level": null,
|
||||||
|
"paymentMethod": "SEPA_DEBIT"
|
||||||
|
}
|
||||||
|
"""));
|
||||||
|
assertThat(response.getStatus()).isEqualTo(400);
|
||||||
|
assertThat(response.hasEntity()).isTrue();
|
||||||
|
|
||||||
|
final Map responseMap = response.readEntity(Map.class);
|
||||||
|
assertThat(responseMap.get("error")).isEqualTo("unsupported_currency");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCreateBoostPaymentIntentLevelAmountMismatch() {
|
void testCreateBoostPaymentIntentLevelAmountMismatch() {
|
||||||
|
when(STRIPE_MANAGER.getSupportedCurrenciesForPaymentMethod(PaymentMethod.CARD))
|
||||||
|
.thenReturn(Set.of("usd", "jpy", "bif", "eur"));
|
||||||
final Response response = RESOURCE_EXTENSION.target("/v1/subscription/boost/create")
|
final Response response = RESOURCE_EXTENSION.target("/v1/subscription/boost/create")
|
||||||
.request()
|
.request()
|
||||||
.post(Entity.json("""
|
.post(Entity.json("""
|
||||||
|
@ -171,9 +217,10 @@ class SubscriptionControllerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCreateBoostPaymentIntent() {
|
void testCreateBoostPaymentIntent() {
|
||||||
|
when(STRIPE_MANAGER.getSupportedCurrenciesForPaymentMethod(PaymentMethod.CARD))
|
||||||
|
.thenReturn(Set.of("usd", "jpy", "bif", "eur"));
|
||||||
when(STRIPE_MANAGER.createPaymentIntent(anyString(), anyLong(), anyLong()))
|
when(STRIPE_MANAGER.createPaymentIntent(anyString(), anyLong(), anyLong()))
|
||||||
.thenReturn(CompletableFuture.completedFuture(PAYMENT_INTENT));
|
.thenReturn(CompletableFuture.completedFuture(PAYMENT_INTENT));
|
||||||
when(STRIPE_MANAGER.supportsCurrency("usd")).thenReturn(true);
|
|
||||||
|
|
||||||
String clientSecret = "some_client_secret";
|
String clientSecret = "some_client_secret";
|
||||||
when(PAYMENT_INTENT.getClientSecret()).thenReturn(clientSecret);
|
when(PAYMENT_INTENT.getClientSecret()).thenReturn(clientSecret);
|
||||||
|
@ -643,7 +690,6 @@ class SubscriptionControllerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getSubscriptionConfiguration() {
|
void getSubscriptionConfiguration() {
|
||||||
|
|
||||||
when(BADGE_TRANSLATOR.translate(any(), eq("B1"))).thenReturn(new Badge("B1", "cat1", "name1", "desc1",
|
when(BADGE_TRANSLATOR.translate(any(), eq("B1"))).thenReturn(new Badge("B1", "cat1", "name1", "desc1",
|
||||||
List.of("l", "m", "h", "x", "xx", "xxx"), "SVG",
|
List.of("l", "m", "h", "x", "xx", "xxx"), "SVG",
|
||||||
List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld"))));
|
List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld"))));
|
||||||
|
@ -667,7 +713,7 @@ class SubscriptionControllerTest {
|
||||||
.request()
|
.request()
|
||||||
.get(GetSubscriptionConfigurationResponse.class);
|
.get(GetSubscriptionConfigurationResponse.class);
|
||||||
|
|
||||||
assertThat(response.currencies()).containsKeys("usd", "jpy", "bif").satisfies(currencyMap -> {
|
assertThat(response.currencies()).containsKeys("usd", "jpy", "bif", "eur").satisfies(currencyMap -> {
|
||||||
assertThat(currencyMap).extractingByKey("usd").satisfies(currency -> {
|
assertThat(currencyMap).extractingByKey("usd").satisfies(currency -> {
|
||||||
assertThat(currency.minimum()).isEqualByComparingTo(
|
assertThat(currency.minimum()).isEqualByComparingTo(
|
||||||
BigDecimal.valueOf(2.5).setScale(2, RoundingMode.HALF_EVEN));
|
BigDecimal.valueOf(2.5).setScale(2, RoundingMode.HALF_EVEN));
|
||||||
|
@ -709,6 +755,19 @@ class SubscriptionControllerTest {
|
||||||
Map.of("5", BigDecimal.valueOf(5000), "15", BigDecimal.valueOf(15000), "35", BigDecimal.valueOf(35000)));
|
Map.of("5", BigDecimal.valueOf(5000), "15", BigDecimal.valueOf(15000), "35", BigDecimal.valueOf(35000)));
|
||||||
assertThat(currency.supportedPaymentMethods()).isEqualTo(List.of("CARD"));
|
assertThat(currency.supportedPaymentMethods()).isEqualTo(List.of("CARD"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
assertThat(currencyMap).extractingByKey("eur").satisfies(currency -> {
|
||||||
|
assertThat(currency.minimum()).isEqualByComparingTo(
|
||||||
|
BigDecimal.valueOf(3));
|
||||||
|
assertThat(currency.oneTime()).isEqualTo(
|
||||||
|
Map.of("1",
|
||||||
|
List.of(BigDecimal.valueOf(5), BigDecimal.valueOf(10),
|
||||||
|
BigDecimal.valueOf(20), BigDecimal.valueOf(30), BigDecimal.valueOf(50), BigDecimal.valueOf(100)), "100",
|
||||||
|
List.of(BigDecimal.valueOf(5))));
|
||||||
|
assertThat(currency.subscription()).isEqualTo(
|
||||||
|
Map.of("5", BigDecimal.valueOf(5), "15", BigDecimal.valueOf(15),"35", BigDecimal.valueOf(35)));
|
||||||
|
assertThat(currency.supportedPaymentMethods()).isEqualTo(List.of("CARD", "SEPA_DEBIT"));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
assertThat(response.levels()).containsKeys("1", "5", "15", "35", "100").satisfies(levelsMap -> {
|
assertThat(response.levels()).containsKeys("1", "5", "15", "35", "100").satisfies(levelsMap -> {
|
||||||
|
@ -821,6 +880,11 @@ class SubscriptionControllerTest {
|
||||||
processorIds:
|
processorIds:
|
||||||
STRIPE: S1
|
STRIPE: S1
|
||||||
BRAINTREE: O1
|
BRAINTREE: O1
|
||||||
|
eur:
|
||||||
|
amount: '5'
|
||||||
|
processorIds:
|
||||||
|
STRIPE: A1
|
||||||
|
BRAINTREE: B1
|
||||||
15:
|
15:
|
||||||
badge: B2
|
badge: B2
|
||||||
prices:
|
prices:
|
||||||
|
@ -839,6 +903,11 @@ class SubscriptionControllerTest {
|
||||||
processorIds:
|
processorIds:
|
||||||
STRIPE: S2
|
STRIPE: S2
|
||||||
BRAINTREE: O2
|
BRAINTREE: O2
|
||||||
|
eur:
|
||||||
|
amount: '15'
|
||||||
|
processorIds:
|
||||||
|
STRIPE: A2
|
||||||
|
BRAINTREE: B2
|
||||||
35:
|
35:
|
||||||
badge: B3
|
badge: B3
|
||||||
prices:
|
prices:
|
||||||
|
@ -857,6 +926,11 @@ class SubscriptionControllerTest {
|
||||||
processorIds:
|
processorIds:
|
||||||
STRIPE: S3
|
STRIPE: S3
|
||||||
BRAINTREE: O3
|
BRAINTREE: O3
|
||||||
|
eur:
|
||||||
|
amount: '35'
|
||||||
|
processorIds:
|
||||||
|
STRIPE: A3
|
||||||
|
BRAINTREE: B3
|
||||||
""";
|
""";
|
||||||
|
|
||||||
private static final String ONETIME_CONFIG_YAML = """
|
private static final String ONETIME_CONFIG_YAML = """
|
||||||
|
@ -879,6 +953,16 @@ class SubscriptionControllerTest {
|
||||||
- '8'
|
- '8'
|
||||||
- '9'
|
- '9'
|
||||||
- '10'
|
- '10'
|
||||||
|
eur:
|
||||||
|
minimum: '3'
|
||||||
|
gift: '5'
|
||||||
|
boosts:
|
||||||
|
- '5'
|
||||||
|
- '10'
|
||||||
|
- '20'
|
||||||
|
- '30'
|
||||||
|
- '50'
|
||||||
|
- '100'
|
||||||
jpy:
|
jpy:
|
||||||
minimum: '250'
|
minimum: '250'
|
||||||
gift: '2000'
|
gift: '2000'
|
||||||
|
@ -899,6 +983,7 @@ class SubscriptionControllerTest {
|
||||||
- '8000'
|
- '8000'
|
||||||
- '9000'
|
- '9000'
|
||||||
- '10000'
|
- '10000'
|
||||||
|
sepaMaxTransactionSizeEuros: '10000'
|
||||||
""";
|
""";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ class BraintreeManagerTest {
|
||||||
void setup() {
|
void setup() {
|
||||||
braintreeGateway = mock(BraintreeGateway.class);
|
braintreeGateway = mock(BraintreeGateway.class);
|
||||||
braintreeManager = new BraintreeManager(braintreeGateway,
|
braintreeManager = new BraintreeManager(braintreeGateway,
|
||||||
Set.of("usd"),
|
Map.of(PaymentMethod.CARD, Set.of("usd")),
|
||||||
Map.of("usd", "usdMerchant"),
|
Map.of("usd", "usdMerchant"),
|
||||||
mock(BraintreeGraphqlClient.class),
|
mock(BraintreeGraphqlClient.class),
|
||||||
Executors.newSingleThreadExecutor());
|
Executors.newSingleThreadExecutor());
|
||||||
|
|
Loading…
Reference in New Issue