Skip to content

Commit

Permalink
Define an endpoint to set the default payment method for iDEAL subscr…
Browse files Browse the repository at this point in the history
…iptions
  • Loading branch information
katherine-signal authored Oct 19, 2023
1 parent 5990a10 commit 8ec062f
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -435,16 +435,7 @@ public CompletableFuture<Response> setDefaultPaymentMethodWithProcessor(

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) {
Expand Down Expand Up @@ -987,6 +978,33 @@ public CompletableFuture<Response> createSubscriptionReceiptCredentials(
});
}

@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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
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;
Expand Down Expand Up @@ -63,7 +64,9 @@
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;
Expand Down Expand Up @@ -632,6 +635,33 @@ public CompletableFuture<Collection<InvoiceLineItem>> getInvoiceLineItemsForInvo
}, 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
Expand Down

0 comments on commit 8ec062f

Please sign in to comment.