Handle stripe amount_too_large errors

This commit is contained in:
ravi-signal 2024-12-18 18:46:22 -06:00 committed by GitHub
parent 68f27be7cd
commit 77658415b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 56 additions and 15 deletions

View File

@ -10,6 +10,11 @@ import io.dropwizard.auth.Auth;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.StringToClassMapItem;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty;
@ -109,25 +114,54 @@ public class OneTimeDonationController {
public static class CreateBoostRequest {
@Schema(required = true, maxLength = 3, minLength = 3)
@NotEmpty
@ExactlySize(3)
public String currency;
@Schema(required = true, minimum = "1", description = "The amount to pay in the [currency's minor unit](https://docs.stripe.com/currencies#minor-units)")
@Min(1)
public long amount;
@Schema(description = "The level for the boost payment. Assumed to be the boost level if missing")
public Long level;
@Schema(description = "The payment method", defaultValue = "CARD")
public PaymentMethod paymentMethod = PaymentMethod.CARD;
}
public record CreateBoostResponse(String clientSecret) {}
public record CreateBoostResponse(
@Schema(description = "A client secret that can be used to complete a stripe PaymentIntent")
String clientSecret) {}
/**
* Creates a Stripe PaymentIntent with the requested amount and currency
*/
@POST
@Path("/create")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Create a Stripe payment intent", description = """
Create a Stripe PaymentIntent and return a client secret that can be used to complete the payment.
Once the payment is complete, the paymentIntentId can be used at /v1/subscriptions/receipt_credentials
""")
@ApiResponse(responseCode = "200", description = "Payment Intent created", content = @Content(schema = @Schema(implementation = CreateBoostResponse.class)))
@ApiResponse(responseCode = "403", description = "The request was made on an authenticated channel")
@ApiResponse(responseCode = "400", description = """
Invalid argument. The response body may include an error code with more specific information. If the error code
is `amount_below_currency_minimum` the body will also include the `minimum` field indicating the minimum amount
for the currency. If the error code is `amount_above_sepa_limit` the body will also include the `maximum`
field indicating the maximum amount for a SEPA transaction.
""",
content = @Content(schema = @Schema(
type = "object",
properties = {
@StringToClassMapItem(key = "error", value = String.class)
})))
@ApiResponse(responseCode = "409", description = "Provided level does not match the currency/amount combination",
content = @Content(schema = @Schema(
type = "object",
properties = {
@StringToClassMapItem(key = "error", value = String.class)
})))
public CompletableFuture<Response> createBoostPaymentIntent(
@ReadOnly @Auth Optional<AuthenticatedDevice> authenticatedAccount,
@NotNull @Valid CreateBoostRequest request,

View File

@ -22,10 +22,10 @@ public class SubscriptionExceptionMapper implements ExceptionMapper<Subscription
public Response toResponse(final SubscriptionException exception) {
// Some exceptions have specific error body formats
if (exception instanceof SubscriptionException.AmountTooSmall e) {
if (exception instanceof SubscriptionException.InvalidAmount e) {
return Response
.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "amount_too_small"))
.entity(Map.of("error", e.getErrorCode()))
.type(MediaType.APPLICATION_JSON_TYPE)
.build();
}

View File

@ -65,10 +65,16 @@ public class SubscriptionException extends Exception {
}
}
public static class AmountTooSmall extends InvalidArguments {
public static class InvalidAmount extends InvalidArguments {
private String errorCode;
public AmountTooSmall() {
public InvalidAmount(String errorCode) {
super(null, null);
this.errorCode = errorCode;
}
public String getErrorCode() {
return errorCode;
}
}

View File

@ -202,8 +202,8 @@ public class StripeManager implements CustomerAwareSubscriptionPaymentProcessor
}
/**
* Creates a payment intent. May throw a
* {@link SubscriptionException.AmountTooSmall} if the amount is too small.
* Creates a payment intent. May throw a {@link SubscriptionException.InvalidAmount} if stripe rejects the
* attempt if the amount is too large or too small
*/
public CompletableFuture<PaymentIntent> createPaymentIntent(final String currency,
final long amount,
@ -224,10 +224,11 @@ public class StripeManager implements CustomerAwareSubscriptionPaymentProcessor
try {
return stripeClient.paymentIntents().create(builder.build(), commonOptions());
} catch (StripeException e) {
if ("amount_too_small".equalsIgnoreCase(e.getCode())) {
throw ExceptionUtils.wrap(new SubscriptionException.AmountTooSmall());
} else {
throw new CompletionException(e);
final String errorCode = e.getCode().toLowerCase(Locale.ROOT);
switch (errorCode) {
case "amount_too_small","amount_too_large" ->
throw ExceptionUtils.wrap(new SubscriptionException.InvalidAmount(errorCode));
default -> throw new CompletionException(e);
}
}
}, executor);