diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/StripeManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/StripeManager.java index 4cff80644..1f13c77f9 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/StripeManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/StripeManager.java @@ -433,33 +433,48 @@ public class StripeManager implements CustomerAwareSubscriptionPaymentProcessor } } - private Subscription endSubscription(Subscription subscription) { + @VisibleForTesting + void endSubscription(Subscription subscription) { + final SubscriptionStatus status = SubscriptionStatus.forApiValue(subscription.getStatus()); - return switch (status) { + switch (status) { // The payment for this period has not processed yet, we should immediately cancel to prevent any payment from // going through. case UNPAID, PAST_DUE, INCOMPLETE -> cancelSubscriptionImmediately(subscription); // Otherwise, set the subscription to cancel at the current period end. If the subscription is active, it may // continue to be used until the end of the period. - default -> cancelSubscriptionAtEndOfCurrentPeriod(subscription); + default -> { + + final Price price = getPriceForSubscription(subscription); + + if (supportedCurrenciesByPaymentMethod.values().stream() + .noneMatch(supported -> supported.contains(price.getCurrency()))) { + + // This currency is no longer supported. Cancel-at-period-end will fail, so we must cancel immediately. + cancelSubscriptionImmediately(subscription); + } else { + + cancelSubscriptionAtEndOfCurrentPeriod(subscription); + } + } }; } - private Subscription cancelSubscriptionImmediately(Subscription subscription) { + private void cancelSubscriptionImmediately(Subscription subscription) { SubscriptionCancelParams params = SubscriptionCancelParams.builder().build(); try { - return stripeClient.v1().subscriptions().cancel(subscription.getId(), params, commonOptions()); + stripeClient.v1().subscriptions().cancel(subscription.getId(), params, commonOptions()); } catch (StripeException e) { throw new UncheckedIOException(new IOException(e)); } } - private Subscription cancelSubscriptionAtEndOfCurrentPeriod(Subscription subscription) { + private void cancelSubscriptionAtEndOfCurrentPeriod(Subscription subscription) { SubscriptionUpdateParams params = SubscriptionUpdateParams.builder() .setCancelAtPeriodEnd(true) .build(); try { - return stripeClient.v1().subscriptions().update(subscription.getId(), params, commonOptions()); + stripeClient.v1().subscriptions().update(subscription.getId(), params, commonOptions()); } catch (StripeException e) { throw new UncheckedIOException(new IOException(e)); } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/subscriptions/StripeManagerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/subscriptions/StripeManagerTest.java index d7759689a..2234c3d50 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/subscriptions/StripeManagerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/subscriptions/StripeManagerTest.java @@ -6,30 +6,46 @@ package org.whispersystems.textsecuregcm.subscriptions; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.assertArg; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.stripe.StripeClient; import com.stripe.exception.ApiException; import com.stripe.exception.StripeException; +import com.stripe.model.Price; +import com.stripe.model.StripeCollection; +import com.stripe.model.Subscription; +import com.stripe.model.SubscriptionItem; +import com.stripe.param.SubscriptionUpdateParams; +import com.stripe.service.SubscriptionItemService; import com.stripe.service.SubscriptionService; +import com.stripe.service.V1Services; import java.nio.charset.StandardCharsets; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import com.stripe.service.V1Services; +import java.util.function.Consumer; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; class StripeManagerTest { private StripeClient stripeClient; private SubscriptionService subscriptionService; + private SubscriptionItemService subscriptionItemsService; private StripeManager stripeManager; private ExecutorService executor; @@ -49,6 +65,8 @@ class StripeManagerTest { subscriptionService = mock(SubscriptionService.class); when(v1Services.subscriptions()).thenReturn(subscriptionService); + subscriptionItemsService = mock(SubscriptionItemService.class); + when(v1Services.subscriptionItems()).thenReturn(subscriptionItemsService); } @AfterEach @@ -66,4 +84,41 @@ class StripeManagerTest { assertThatExceptionOfType(SubscriptionPaymentRequiresActionException.class).isThrownBy(() -> stripeManager.createSubscription("customerId", "priceId", 1, 0)); } + + @ParameterizedTest + @CsvSource( + { + "usd, unpaid, true", + "usd, past_due, true", + "usd, incomplete, true", + "usd, active, false", + "zzz, active, true", + } + ) + void testEndSubscription(final String currency, final String status, final boolean expectCancelImmediately) throws Exception { + final Subscription subscription = mock(Subscription.class); + when(subscription.getId()).thenReturn("test-subscription"); + when(subscription.getStatus()).thenReturn(status); + + final SubscriptionItem item = mock(SubscriptionItem.class); + final Price price = new Price(); + price.setCurrency(currency); + when(item.getPrice()).thenReturn(price); + + @SuppressWarnings("unchecked") final StripeCollection items = mock(StripeCollection.class); + when(items.autoPagingIterable()).thenReturn(List.of(item)); + when(subscriptionItemsService.list(any(), any())) + .thenReturn(items); + + stripeManager.endSubscription(subscription); + + verify(subscriptionService, expectCancelImmediately ? times(1) : never()) + .cancel(any(), any(), any()); + verify(subscriptionService, expectCancelImmediately ? never() : times(1)) + .update(any(), assertArg( + (Consumer) params -> assertTrue(params.getCancelAtPeriodEnd())), + any()); + + } + }