Cancel past_due subscriptions immediately

This commit is contained in:
Ravi Khadiwala 2024-11-20 12:45:28 -06:00 committed by ravi-signal
parent 815fd44ab3
commit d135957f0d
2 changed files with 62 additions and 27 deletions

View File

@ -453,7 +453,7 @@ public class BraintreeManager implements CustomerAwareSubscriptionPaymentProcess
// since badge redemption is untrackable by design and unrevokable, subscription changes must be immediate and // 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, // not prorated. Braintree subscriptions cannot change their next billing date,
// so we must end the existing one and create a new one // so we must end the existing one and create a new one
return cancelSubscriptionAtEndOfCurrentPeriod(subscription) return endSubscription(subscription)
.thenCompose(ignored -> { .thenCompose(ignored -> {
final Transaction transaction = getLatestTransactionForSubscription(subscription) final Transaction transaction = getLatestTransactionForSubscription(subscription)
@ -504,19 +504,9 @@ public class BraintreeManager implements CustomerAwareSubscriptionPaymentProcess
final Instant anchor = subscription.getFirstBillingDate().toInstant(); final Instant anchor = subscription.getFirstBillingDate().toInstant();
final Instant endOfCurrentPeriod = subscription.getBillingPeriodEndDate().toInstant(); final Instant endOfCurrentPeriod = subscription.getBillingPeriodEndDate().toInstant();
boolean paymentProcessing = false; final TransactionInfo latestTransactionInfo = getLatestTransactionForSubscription(subscription)
ChargeFailure chargeFailure = null; .map(this::getTransactionInfo)
.orElse(new TransactionInfo(PaymentMethod.PAYPAL, false, false, null));
final Optional<Transaction> 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());
}
}
return new SubscriptionInformation( return new SubscriptionInformation(
new SubscriptionPrice(plan.getCurrencyIsoCode().toUpperCase(Locale.ROOT), new SubscriptionPrice(plan.getCurrencyIsoCode().toUpperCase(Locale.ROOT),
@ -526,16 +516,31 @@ public class BraintreeManager implements CustomerAwareSubscriptionPaymentProcess
endOfCurrentPeriod, endOfCurrentPeriod,
Subscription.Status.ACTIVE == subscription.getStatus(), Subscription.Status.ACTIVE == subscription.getStatus(),
!subscription.neverExpires(), !subscription.neverExpires(),
getSubscriptionStatus(subscription.getStatus(), latestTransactionFailed), getSubscriptionStatus(subscription.getStatus(), latestTransactionInfo.transactionFailed()),
PaymentProvider.BRAINTREE, PaymentProvider.BRAINTREE,
latestTransaction.map(this::getPaymentMethodFromTransaction).orElse(PaymentMethod.PAYPAL), latestTransactionInfo.paymentMethod(),
paymentProcessing, latestTransactionInfo.paymentProcessing(),
chargeFailure latestTransactionInfo.chargeFailure()
); );
}, executor); }, 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) { private PaymentMethod getPaymentMethodFromTransaction(Transaction transaction) {
if (transaction.getPayPalDetails() != null) { if (transaction.getPayPalDetails() != null) {
return PaymentMethod.PAYPAL; return PaymentMethod.PAYPAL;
@ -583,19 +588,37 @@ public class BraintreeManager implements CustomerAwareSubscriptionPaymentProcess
.map(com.braintreegateway.PaymentMethod::getSubscriptions) .map(com.braintreegateway.PaymentMethod::getSubscriptions)
.orElse(Collections.emptyList()) .orElse(Collections.emptyList())
.stream() .stream()
.map(this::cancelSubscriptionAtEndOfCurrentPeriod) .map(this::endSubscription)
.toList(); .toList();
return CompletableFuture.allOf(subscriptionCancelFutures.toArray(new CompletableFuture[0])); return CompletableFuture.allOf(subscriptionCancelFutures.toArray(new CompletableFuture[0]));
}); });
} }
private CompletableFuture<Void> 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<Void> cancelSubscriptionAtEndOfCurrentPeriod(Subscription subscription) { private CompletableFuture<Void> cancelSubscriptionAtEndOfCurrentPeriod(Subscription subscription) {
return CompletableFuture.supplyAsync(() -> { return CompletableFuture.runAsync(() -> braintreeGateway
braintreeGateway.subscription().update(subscription.getId(), .subscription()
new SubscriptionRequest().numberOfBillingCycles(subscription.getCurrentBillingCycle())); .update(subscription.getId(),
return null; new SubscriptionRequest().numberOfBillingCycles(subscription.getCurrentBillingCycle())), executor);
}, executor); }
private CompletableFuture<Void> cancelSubscriptionImmediately(Subscription subscription) {
return CompletableFuture.runAsync(() -> braintreeGateway.subscription().cancel(subscription.getId()), executor);
} }

View File

@ -385,7 +385,7 @@ public class StripeManager implements CustomerAwareSubscriptionPaymentProcessor
}).thenCompose(subscriptions -> { }).thenCompose(subscriptions -> {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
CompletableFuture<Subscription>[] futures = (CompletableFuture<Subscription>[]) subscriptions.stream() CompletableFuture<Subscription>[] futures = (CompletableFuture<Subscription>[]) subscriptions.stream()
.map(this::cancelSubscriptionAtEndOfCurrentPeriod).toArray(CompletableFuture[]::new); .map(this::endSubscription).toArray(CompletableFuture[]::new);
return CompletableFuture.allOf(futures); return CompletableFuture.allOf(futures);
}); });
} }
@ -404,7 +404,19 @@ public class StripeManager implements CustomerAwareSubscriptionPaymentProcessor
}, executor); }, executor);
} }
public CompletableFuture<Subscription> cancelSubscriptionImmediately(Subscription subscription) { private CompletableFuture<Subscription> 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<Subscription> cancelSubscriptionImmediately(Subscription subscription) {
return CompletableFuture.supplyAsync(() -> { return CompletableFuture.supplyAsync(() -> {
SubscriptionCancelParams params = SubscriptionCancelParams.builder().build(); SubscriptionCancelParams params = SubscriptionCancelParams.builder().build();
try { try {
@ -415,7 +427,7 @@ public class StripeManager implements CustomerAwareSubscriptionPaymentProcessor
}, executor); }, executor);
} }
public CompletableFuture<Subscription> cancelSubscriptionAtEndOfCurrentPeriod(Subscription subscription) { private CompletableFuture<Subscription> cancelSubscriptionAtEndOfCurrentPeriod(Subscription subscription) {
return CompletableFuture.supplyAsync(() -> { return CompletableFuture.supplyAsync(() -> {
SubscriptionUpdateParams params = SubscriptionUpdateParams.builder() SubscriptionUpdateParams params = SubscriptionUpdateParams.builder()
.setCancelAtPeriodEnd(true) .setCancelAtPeriodEnd(true)