diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeManager.java index 3f4129f1f..43ef44a70 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeManager.java @@ -453,7 +453,7 @@ public class BraintreeManager implements CustomerAwareSubscriptionPaymentProcess // since badge redemption is untrackable by design and unrevokable, subscription changes must be immediate and // not prorated. Braintree subscriptions cannot change their next billing date, // so we must end the existing one and create a new one - return cancelSubscriptionAtEndOfCurrentPeriod(subscription) + return endSubscription(subscription) .thenCompose(ignored -> { final Transaction transaction = getLatestTransactionForSubscription(subscription) @@ -504,19 +504,9 @@ public class BraintreeManager implements CustomerAwareSubscriptionPaymentProcess final Instant anchor = subscription.getFirstBillingDate().toInstant(); final Instant endOfCurrentPeriod = subscription.getBillingPeriodEndDate().toInstant(); - boolean paymentProcessing = false; - ChargeFailure chargeFailure = null; - - final Optional latestTransaction = getLatestTransactionForSubscription(subscription); - - boolean latestTransactionFailed = false; - if (latestTransaction.isPresent()){ - paymentProcessing = isPaymentProcessing(latestTransaction.get().getStatus()); - if (getPaymentStatus(latestTransaction.get().getStatus()) != PaymentStatus.SUCCEEDED) { - latestTransactionFailed = true; - chargeFailure = createChargeFailure(latestTransaction.get()); - } - } + final TransactionInfo latestTransactionInfo = getLatestTransactionForSubscription(subscription) + .map(this::getTransactionInfo) + .orElse(new TransactionInfo(PaymentMethod.PAYPAL, false, false, null)); return new SubscriptionInformation( new SubscriptionPrice(plan.getCurrencyIsoCode().toUpperCase(Locale.ROOT), @@ -526,16 +516,31 @@ public class BraintreeManager implements CustomerAwareSubscriptionPaymentProcess endOfCurrentPeriod, Subscription.Status.ACTIVE == subscription.getStatus(), !subscription.neverExpires(), - getSubscriptionStatus(subscription.getStatus(), latestTransactionFailed), + getSubscriptionStatus(subscription.getStatus(), latestTransactionInfo.transactionFailed()), PaymentProvider.BRAINTREE, - latestTransaction.map(this::getPaymentMethodFromTransaction).orElse(PaymentMethod.PAYPAL), - paymentProcessing, - chargeFailure + latestTransactionInfo.paymentMethod(), + latestTransactionInfo.paymentProcessing(), + latestTransactionInfo.chargeFailure() ); }, executor); } + private record TransactionInfo( + PaymentMethod paymentMethod, + boolean paymentProcessing, + boolean transactionFailed, + @Nullable ChargeFailure chargeFailure) {} + + private TransactionInfo getTransactionInfo(final Transaction transaction) { + final boolean paymentProcessing = isPaymentProcessing(transaction.getStatus()); + final PaymentMethod paymentMethod = getPaymentMethodFromTransaction(transaction); + if (getPaymentStatus(transaction.getStatus()) != PaymentStatus.SUCCEEDED) { + return new TransactionInfo(paymentMethod, paymentProcessing, true, createChargeFailure(transaction)); + } + return new TransactionInfo(paymentMethod, paymentProcessing, false, null); + } + private PaymentMethod getPaymentMethodFromTransaction(Transaction transaction) { if (transaction.getPayPalDetails() != null) { return PaymentMethod.PAYPAL; @@ -583,19 +588,37 @@ public class BraintreeManager implements CustomerAwareSubscriptionPaymentProcess .map(com.braintreegateway.PaymentMethod::getSubscriptions) .orElse(Collections.emptyList()) .stream() - .map(this::cancelSubscriptionAtEndOfCurrentPeriod) + .map(this::endSubscription) .toList(); return CompletableFuture.allOf(subscriptionCancelFutures.toArray(new CompletableFuture[0])); }); } + private CompletableFuture endSubscription(Subscription subscription) { + final boolean latestTransactionFailed = getLatestTransactionForSubscription(subscription) + .map(this::getTransactionInfo) + .map(TransactionInfo::transactionFailed) + .orElse(false); + return switch (getSubscriptionStatus(subscription.getStatus(), latestTransactionFailed)) { + // The payment for this period has not processed yet, we should immediately cancel to prevent any payment from + // going through. + case INCOMPLETE, PAST_DUE, UNPAID -> 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); + }; + } + private CompletableFuture cancelSubscriptionAtEndOfCurrentPeriod(Subscription subscription) { - return CompletableFuture.supplyAsync(() -> { - braintreeGateway.subscription().update(subscription.getId(), - new SubscriptionRequest().numberOfBillingCycles(subscription.getCurrentBillingCycle())); - return null; - }, executor); + return CompletableFuture.runAsync(() -> braintreeGateway + .subscription() + .update(subscription.getId(), + new SubscriptionRequest().numberOfBillingCycles(subscription.getCurrentBillingCycle())), executor); + } + + private CompletableFuture cancelSubscriptionImmediately(Subscription subscription) { + return CompletableFuture.runAsync(() -> braintreeGateway.subscription().cancel(subscription.getId()), executor); } 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 095bb289c..d3589f935 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/StripeManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/StripeManager.java @@ -385,7 +385,7 @@ public class StripeManager implements CustomerAwareSubscriptionPaymentProcessor }).thenCompose(subscriptions -> { @SuppressWarnings("unchecked") CompletableFuture[] futures = (CompletableFuture[]) subscriptions.stream() - .map(this::cancelSubscriptionAtEndOfCurrentPeriod).toArray(CompletableFuture[]::new); + .map(this::endSubscription).toArray(CompletableFuture[]::new); return CompletableFuture.allOf(futures); }); } @@ -404,7 +404,19 @@ public class StripeManager implements CustomerAwareSubscriptionPaymentProcessor }, executor); } - public CompletableFuture cancelSubscriptionImmediately(Subscription subscription) { + private CompletableFuture endSubscription(Subscription subscription) { + final SubscriptionStatus status = SubscriptionStatus.forApiValue(subscription.getStatus()); + return 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); + }; + } + + private CompletableFuture cancelSubscriptionImmediately(Subscription subscription) { return CompletableFuture.supplyAsync(() -> { SubscriptionCancelParams params = SubscriptionCancelParams.builder().build(); try { @@ -415,7 +427,7 @@ public class StripeManager implements CustomerAwareSubscriptionPaymentProcessor }, executor); } - public CompletableFuture cancelSubscriptionAtEndOfCurrentPeriod(Subscription subscription) { + private CompletableFuture cancelSubscriptionAtEndOfCurrentPeriod(Subscription subscription) { return CompletableFuture.supplyAsync(() -> { SubscriptionUpdateParams params = SubscriptionUpdateParams.builder() .setCancelAtPeriodEnd(true)