Define an endpoint to set the default payment method for iDEAL subscriptions

This commit is contained in:
Katherine 2023-10-19 10:29:40 -07:00 committed by GitHub
parent 5990a100db
commit 8ec062fbef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 58 additions and 10 deletions

View File

@ -435,16 +435,7 @@ public class SubscriptionController {
final SubscriptionProcessorManager manager = getManagerForProcessor(processor);
return subscriptionManager.get(requestData.subscriberUser, requestData.hmac)
.thenApply(this::requireRecordFromGetResult)
.thenCompose(record -> record.getProcessorCustomer()
.map(processorCustomer -> manager.setDefaultPaymentMethodForCustomer(processorCustomer.customerId(),
paymentMethodToken, record.subscriptionId))
.orElseThrow(() ->
// a missing customer ID indicates the client made requests out of order,
// and needs to call create_payment_method to create a customer for the given payment method
new ClientErrorException(Status.CONFLICT)))
.thenApply(customer -> Response.ok().build());
return setDefaultPaymentMethod(manager, paymentMethodToken, requestData);
}
public record SetSubscriptionLevelSuccessResponse(long level) {
@ -987,6 +978,33 @@ public class SubscriptionController {
});
}
@POST
@Path("/{subscriberId}/default_payment_method_for_ideal/{setupIntentId}")
@Produces(MediaType.APPLICATION_JSON)
public CompletableFuture<Response> setDefaultPaymentMethodForIdeal(
@Auth Optional<AuthenticatedAccount> authenticatedAccount,
@PathParam("subscriberId") String subscriberId,
@PathParam("setupIntentId") @NotEmpty String setupIntentId) {
RequestData requestData = RequestData.process(authenticatedAccount, subscriberId, clock);
return stripeManager.getGeneratedSepaIdFromSetupIntent(setupIntentId)
.thenCompose(generatedSepaId -> setDefaultPaymentMethod(stripeManager, generatedSepaId, requestData));
}
private CompletableFuture<Response> setDefaultPaymentMethod(final SubscriptionProcessorManager manager,
final String paymentMethodId,
final RequestData requestData) {
return subscriptionManager.get(requestData.subscriberUser, requestData.hmac)
.thenApply(this::requireRecordFromGetResult)
.thenCompose(record -> record.getProcessorCustomer()
.map(processorCustomer -> manager.setDefaultPaymentMethodForCustomer(processorCustomer.customerId(),
paymentMethodId, record.subscriptionId))
.orElseThrow(() ->
// a missing customer ID indicates the client made requests out of order,
// and needs to call create_payment_method to create a customer for the given payment method
new ClientErrorException(Status.CONFLICT)))
.thenApply(customer -> Response.ok().build());
}
private Instant receiptExpirationWithGracePeriod(Instant itemExpiration) {
return itemExpiration.plus(subscriptionConfiguration.getBadgeGracePeriod())
.truncatedTo(ChronoUnit.DAYS)

View File

@ -31,6 +31,7 @@ import com.stripe.param.PaymentIntentCreateParams;
import com.stripe.param.PaymentIntentRetrieveParams;
import com.stripe.param.PriceRetrieveParams;
import com.stripe.param.SetupIntentCreateParams;
import com.stripe.param.SetupIntentRetrieveParams;
import com.stripe.param.SubscriptionCancelParams;
import com.stripe.param.SubscriptionCreateParams;
import com.stripe.param.SubscriptionItemListParams;
@ -63,7 +64,9 @@ import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.ws.rs.ClientErrorException;
import javax.ws.rs.InternalServerErrorException;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
@ -632,6 +635,33 @@ public class StripeManager implements SubscriptionProcessorManager {
}, executor);
}
public CompletableFuture<String> getGeneratedSepaIdFromSetupIntent(String setupIntentId) {
return CompletableFuture.supplyAsync(() -> {
SetupIntentRetrieveParams params = SetupIntentRetrieveParams.builder()
.addExpand("latest_attempt")
.build();
try {
final SetupIntent setupIntent = stripeClient.setupIntents().retrieve(setupIntentId, params, commonOptions());
if (setupIntent.getLatestAttemptObject() == null
|| setupIntent.getLatestAttemptObject().getPaymentMethodDetails() == null
|| setupIntent.getLatestAttemptObject().getPaymentMethodDetails().getIdeal() == null
|| setupIntent.getLatestAttemptObject().getPaymentMethodDetails().getIdeal().getGeneratedSepaDebit() == null) {
// This usually indicates that the client has made requests out of order, either by not confirming
// the SetupIntent or not having the user authorize the transaction.
logger.debug("setupIntent {} missing expected fields", setupIntentId);
throw new ClientErrorException(Status.CONFLICT);
}
return setupIntent.getLatestAttemptObject().getPaymentMethodDetails().getIdeal().getGeneratedSepaDebit();
} catch (StripeException e) {
if (e.getStatusCode() == 404) {
throw new NotFoundException();
}
logger.error("unexpected error from Stripe when retrieving setupIntent {}", setupIntentId, e);
throw new CompletionException(e);
}
}, executor);
}
/**
* We use a client generated idempotency key for subscription updates due to not being able to distinguish between a
* call to update to level 2, then back to level 1, then back to level 2. If this all happens within Stripe's