From 99ad211c0163fbb7887e36e7bed78c7184c0eff8 Mon Sep 17 00:00:00 2001 From: katherine-signal Date: Mon, 28 Nov 2022 11:33:48 -0800 Subject: [PATCH] Enforce minimum amount by currency for one time donations --- .../controllers/SubscriptionController.java | 11 +++- .../SubscriptionControllerTest.java | 54 ++++++++++++++++++- 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java index 2734cfc06..5becbfcdc 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java @@ -712,16 +712,25 @@ public class SubscriptionController { if (request.level == null) { request.level = oneTimeDonationConfiguration.boost().level(); } + BigDecimal amount = BigDecimal.valueOf(request.amount); if (request.level == oneTimeDonationConfiguration.gift().level()) { BigDecimal amountConfigured = oneTimeDonationConfiguration.currencies() .get(request.currency.toLowerCase(Locale.ROOT)).gift(); if (amountConfigured == null || stripeManager.convertConfiguredAmountToStripeAmount(request.currency, amountConfigured) - .compareTo(BigDecimal.valueOf(request.amount)) != 0) { + .compareTo(amount) != 0) { throw new WebApplicationException( Response.status(Status.CONFLICT).entity(Map.of("error", "level_amount_mismatch")).build()); } } + BigDecimal minCurrencyAmountMajorUnits = oneTimeDonationConfiguration.currencies() + .get(request.currency.toLowerCase(Locale.ROOT)).minimum(); + BigDecimal minCurrencyAmountMinorUnits = stripeManager.convertConfiguredAmountToStripeAmount(request.currency, + minCurrencyAmountMajorUnits); + if (minCurrencyAmountMinorUnits.compareTo(amount) > 0) { + throw new BadRequestException(Response.status(Status.BAD_REQUEST) + .entity(Map.of("error", "amount_below_currency_minimum")).build()); + } }) .thenCompose(unused -> stripeManager.createPaymentIntent(request.currency, request.amount, request.level)) .thenApply(paymentIntent -> Response.ok(new CreateBoostResponse(paymentIntent.getClientSecret())).build()); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SubscriptionControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SubscriptionControllerTest.java index 648d5e6b6..5a1319e77 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SubscriptionControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SubscriptionControllerTest.java @@ -8,6 +8,7 @@ package org.whispersystems.textsecuregcm.controllers; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; @@ -19,6 +20,7 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.stripe.exception.ApiException; import com.stripe.model.Subscription; +import com.stripe.model.PaymentIntent; import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; import io.dropwizard.testing.junit5.ResourceExtension; @@ -56,6 +58,7 @@ import org.whispersystems.textsecuregcm.controllers.SubscriptionController.GetLe import org.whispersystems.textsecuregcm.controllers.SubscriptionController.GetSubscriptionConfigurationResponse; import org.whispersystems.textsecuregcm.entities.Badge; import org.whispersystems.textsecuregcm.entities.BadgeSvg; +import org.whispersystems.textsecuregcm.mappers.CompletionExceptionMapper; import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager; import org.whispersystems.textsecuregcm.storage.SubscriptionManager; import org.whispersystems.textsecuregcm.subscriptions.PaymentMethod; @@ -81,6 +84,7 @@ class SubscriptionControllerTest { private static final OneTimeDonationConfiguration ONETIME_CONFIG = ConfigHelper.getOneTimeConfig(); private static final SubscriptionManager SUBSCRIPTION_MANAGER = mock(SubscriptionManager.class); private static final StripeManager STRIPE_MANAGER = mock(StripeManager.class); + private static final PaymentIntent PAYMENT_INTENT = mock(PaymentIntent.class); static { when(STRIPE_MANAGER.getSupportedCurrencies()) @@ -99,6 +103,7 @@ class SubscriptionControllerTest { private static final ResourceExtension RESOURCE_EXTENSION = ResourceExtension.builder() .addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE) .addProvider(AuthHelper.getAuthFilter()) + .addProvider(CompletionExceptionMapper.class) .addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>(Set.of( AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class))) .setMapper(SystemMapper.getMapper()) @@ -117,6 +122,53 @@ class SubscriptionControllerTest { BADGE_TRANSLATOR, LEVEL_TRANSLATOR); } + @Test + void testCreateBoostPaymentIntentAmountBelowCurrencyMinimum() { + when(STRIPE_MANAGER.convertConfiguredAmountToStripeAmount(any(), any())).thenReturn(new BigDecimal(250)); + final Response response = RESOURCE_EXTENSION.target("/v1/subscription/boost/create") + .request() + .post(Entity.json(""" + { + "currency": "USD", + "amount": 249, + "level": null + } + """)); + assertThat(response.getStatus()).isEqualTo(400); + } + + @Test + void testCreateBoostPaymentIntentLevelAmountMismatch() { + when(STRIPE_MANAGER.convertConfiguredAmountToStripeAmount(any(), any())).thenReturn(new BigDecimal(20)); + + final Response response = RESOURCE_EXTENSION.target("/v1/subscription/boost/create") + .request() + .post(Entity.json(""" + { + "currency": "USD", + "amount": 25, + "level": 100 + } + """ + )); + assertThat(response.getStatus()).isEqualTo(409); + } + + @Test + void testCreateBoostPaymentIntent() { + when(STRIPE_MANAGER.convertConfiguredAmountToStripeAmount(any(), any())).thenReturn(new BigDecimal(300)); + when(STRIPE_MANAGER.createPaymentIntent(anyString(), anyLong(), anyLong())) + .thenReturn(CompletableFuture.completedFuture(PAYMENT_INTENT)); + + String clientSecret = "some_client_secret"; + when(PAYMENT_INTENT.getClientSecret()).thenReturn(clientSecret); + + final Response response = RESOURCE_EXTENSION.target("/v1/subscription/boost/create") + .request() + .post(Entity.json("{\"currency\": \"USD\", \"amount\": 300, \"level\": null}")); + assertThat(response.getStatus()).isEqualTo(200); + } + @Test void createBoostReceiptInvalid() { final Response response = RESOURCE_EXTENSION.target("/v1/subscription/boost/receipt_credentials") @@ -649,7 +701,7 @@ class SubscriptionControllerTest { badge: GIFT currencies: usd: - minimum: '2.50' + minimum: '2.50' # fractional to test BigDecimal conversion gift: '20' boosts: - '5.50'