From 63be7b93ce4f609ca1581333abbd0a219edd1e36 Mon Sep 17 00:00:00 2001 From: Ehren Kret Date: Fri, 29 Apr 2022 12:04:42 -0500 Subject: [PATCH] Record level on boost payment intent --- .../controllers/SubscriptionController.java | 90 +++++++++---------- .../textsecuregcm/stripe/StripeManager.java | 3 +- 2 files changed, 44 insertions(+), 49 deletions(-) diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java index 7e8cbd53f..53f5c4604 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java @@ -25,6 +25,7 @@ import java.math.BigDecimal; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.time.Clock; +import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Base64; @@ -494,28 +495,9 @@ public class SubscriptionController { } public static class CreateBoostRequest { - - private final String currency; - private final long amount; - - @JsonCreator - public CreateBoostRequest( - @JsonProperty("currency") String currency, - @JsonProperty("amount") long amount) { - this.currency = currency; - this.amount = amount; - } - - @NotEmpty - @ExactlySize(3) - public String getCurrency() { - return currency; - } - - @Min(1) - public long getAmount() { - return amount; - } + @NotEmpty @ExactlySize(3) public String currency; + @Min(1) public long amount; + public Long level; } public static class CreateBoostResponse { @@ -539,32 +521,24 @@ public class SubscriptionController { @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public CompletableFuture createBoostPaymentIntent(@NotNull @Valid CreateBoostRequest request) { - return stripeManager.createPaymentIntent(request.getCurrency(), request.getAmount()) + return CompletableFuture.runAsync(() -> { + if (request.level == null) { + request.level = boostConfiguration.getLevel(); + } + if (request.level == giftConfiguration.level()) { + BigDecimal amountConfigured = giftConfiguration.currencies().get(request.currency.toLowerCase(Locale.ROOT)); + if (amountConfigured == null || !amountConfigured.equals(BigDecimal.valueOf(request.amount))) { + throw new WebApplicationException(Response.status(Status.CONFLICT).entity(Map.of("error", "level_amount_mismatch")).build()); + } + } + }) + .thenCompose(unused -> stripeManager.createPaymentIntent(request.currency, request.amount, request.level)) .thenApply(paymentIntent -> Response.ok(new CreateBoostResponse(paymentIntent.getClientSecret())).build()); } public static class CreateBoostReceiptCredentialsRequest { - - private final String paymentIntentId; - private final byte[] receiptCredentialRequest; - - @JsonCreator - public CreateBoostReceiptCredentialsRequest( - @JsonProperty("paymentIntentId") String paymentIntentId, - @JsonProperty("receiptCredentialRequest") byte[] receiptCredentialRequest) { - this.paymentIntentId = paymentIntentId; - this.receiptCredentialRequest = receiptCredentialRequest; - } - - @NotNull - public String getPaymentIntentId() { - return paymentIntentId; - } - - @NotNull - public byte[] getReceiptCredentialRequest() { - return receiptCredentialRequest; - } + @NotNull public String paymentIntentId; + @NotNull public byte[] receiptCredentialRequest; } public static class CreateBoostReceiptCredentialsResponse { @@ -588,7 +562,7 @@ public class SubscriptionController { @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public CompletableFuture createBoostReceiptCredentials(@NotNull @Valid CreateBoostReceiptCredentialsRequest request) { - return stripeManager.getPaymentIntent(request.getPaymentIntentId()) + return stripeManager.getPaymentIntent(request.paymentIntentId) .thenCompose(paymentIntent -> { if (paymentIntent == null) { throw new WebApplicationException(Status.NOT_FOUND); @@ -599,22 +573,42 @@ public class SubscriptionController { if (!StringUtils.equalsIgnoreCase("succeeded", paymentIntent.getStatus())) { throw new WebApplicationException(Status.PAYMENT_REQUIRED); } + long level = boostConfiguration.getLevel(); + if (paymentIntent.getMetadata() != null) { + String levelMetadata = paymentIntent.getMetadata().getOrDefault("level", Long.toString(boostConfiguration.getLevel())); + try { + level = Long.parseLong(levelMetadata); + } catch (NumberFormatException e) { + logger.error("failed to parse level metadata ({}) on payment intent {}", levelMetadata, paymentIntent.getId(), e); + throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR); + } + } + Duration levelExpiration; + if (boostConfiguration.getLevel() == level) { + levelExpiration = boostConfiguration.getExpiration(); + } else if (giftConfiguration.level() == level) { + levelExpiration = giftConfiguration.expiration(); + } else { + logger.error("level ({}) returned from payment intent that is unknown to the server", level); + throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR); + } ReceiptCredentialRequest receiptCredentialRequest; try { - receiptCredentialRequest = new ReceiptCredentialRequest(request.getReceiptCredentialRequest()); + receiptCredentialRequest = new ReceiptCredentialRequest(request.receiptCredentialRequest); } catch (InvalidInputException e) { throw new BadRequestException("invalid receipt credential request", e); } + final long finalLevel = level; return issuedReceiptsManager.recordIssuance(paymentIntent.getId(), receiptCredentialRequest, clock.instant()) .thenApply(unused -> { Instant expiration = Instant.ofEpochSecond(paymentIntent.getCreated()) - .plus(boostConfiguration.getExpiration()) + .plus(levelExpiration) .truncatedTo(ChronoUnit.DAYS) .plus(1, ChronoUnit.DAYS); ReceiptCredentialResponse receiptCredentialResponse; try { receiptCredentialResponse = zkReceiptOperations.issueReceiptCredential( - receiptCredentialRequest, expiration.getEpochSecond(), boostConfiguration.getLevel()); + receiptCredentialRequest, expiration.getEpochSecond(), finalLevel); } catch (VerificationFailedException e) { throw new BadRequestException("receipt credential request failed verification", e); } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/stripe/StripeManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/stripe/StripeManager.java index a567aaa7b..40719743d 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/stripe/StripeManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/stripe/StripeManager.java @@ -154,12 +154,13 @@ public class StripeManager { /** * Creates a payment intent. May throw a 400 WebApplicationException if the amount is too small. */ - public CompletableFuture createPaymentIntent(String currency, long amount) { + public CompletableFuture createPaymentIntent(String currency, long amount, long level) { return CompletableFuture.supplyAsync(() -> { PaymentIntentCreateParams params = PaymentIntentCreateParams.builder() .setAmount(amount) .setCurrency(currency.toLowerCase(Locale.ROOT)) .setDescription(boostDescription) + .putMetadata("level", Long.toString(level)) .build(); try { return PaymentIntent.create(params, commonOptions());