From 77658415b28ca8173fb15b21249e4c8a3864b144 Mon Sep 17 00:00:00 2001 From: ravi-signal <99042880+ravi-signal@users.noreply.github.com> Date: Wed, 18 Dec 2024 18:46:22 -0600 Subject: [PATCH] Handle stripe amount_too_large errors --- .../OneTimeDonationController.java | 44 ++++++++++++++++--- .../mappers/SubscriptionExceptionMapper.java | 4 +- .../storage/SubscriptionException.java | 10 ++++- .../subscriptions/StripeManager.java | 13 +++--- 4 files changed, 56 insertions(+), 15 deletions(-) diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/OneTimeDonationController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/OneTimeDonationController.java index 957ebffe6..a8c80c238 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/OneTimeDonationController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/OneTimeDonationController.java @@ -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 createBoostPaymentIntent( @ReadOnly @Auth Optional authenticatedAccount, @NotNull @Valid CreateBoostRequest request, diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/mappers/SubscriptionExceptionMapper.java b/service/src/main/java/org/whispersystems/textsecuregcm/mappers/SubscriptionExceptionMapper.java index 4511fdf1e..dcd08d171 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/mappers/SubscriptionExceptionMapper.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/mappers/SubscriptionExceptionMapper.java @@ -22,10 +22,10 @@ public class SubscriptionExceptionMapper implements ExceptionMapper 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);