From 626a7fdad7e4c1d4c6d6b47066b5c82a977d77e6 Mon Sep 17 00:00:00 2001 From: Ravi Khadiwala Date: Thu, 12 Jun 2025 15:54:26 -0500 Subject: [PATCH] Add docs to /v1/donations/redeem-receipt --- .../controllers/DonationController.java | 44 ++++++++++++++----- .../entities/RedeemReceiptRequest.java | 4 ++ 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DonationController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DonationController.java index 0c10854d7..de6e54102 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DonationController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DonationController.java @@ -6,6 +6,8 @@ package org.whispersystems.textsecuregcm.controllers; import io.dropwizard.auth.Auth; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; @@ -70,21 +72,40 @@ public class DonationController { @Path("/redeem-receipt") @Consumes(MediaType.APPLICATION_JSON) @Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) + @Operation( + summary = "Redeem receipt", + description = """ + Redeem a receipt acquired from /v1/subscription/{subscriberId}/receipt_credentials to add a badge to the + account. After successful redemption, profile responses will include the corresponding badge (if configured as + visible) until the expiration time on the receipt. + """) + @ApiResponse(responseCode = "200", description = "The receipt was redeemed") + @ApiResponse(responseCode = "400", description = """ + The provided presentation or receipt was invalid, or the receipt was already redeemed for a different account. A + specific error message suitable for logging will be included as text/plain body + """) + @ApiResponse(responseCode = "429", description = "Rate limited.") public CompletionStage redeemReceipt( @Mutable @Auth final AuthenticatedDevice auth, @NotNull @Valid final RedeemReceiptRequest request) { return CompletableFuture.supplyAsync(() -> { ReceiptCredentialPresentation receiptCredentialPresentation; try { - receiptCredentialPresentation = receiptCredentialPresentationFactory.build( - request.getReceiptCredentialPresentation()); + receiptCredentialPresentation = receiptCredentialPresentationFactory + .build(request.getReceiptCredentialPresentation()); } catch (InvalidInputException e) { - return CompletableFuture.completedFuture(Response.status(Status.BAD_REQUEST).entity("invalid receipt credential presentation").type(MediaType.TEXT_PLAIN_TYPE).build()); + return CompletableFuture.completedFuture(Response.status(Status.BAD_REQUEST) + .entity("invalid receipt credential presentation") + .type(MediaType.TEXT_PLAIN_TYPE) + .build()); } try { serverZkReceiptOperations.verifyReceiptCredentialPresentation(receiptCredentialPresentation); } catch (VerificationFailedException e) { - return CompletableFuture.completedFuture(Response.status(Status.BAD_REQUEST).entity("receipt credential presentation verification failed").type(MediaType.TEXT_PLAIN_TYPE).build()); + return CompletableFuture.completedFuture(Response.status(Status.BAD_REQUEST) + .entity("receipt credential presentation verification failed") + .type(MediaType.TEXT_PLAIN_TYPE) + .build()); } final ReceiptSerial receiptSerial = receiptCredentialPresentation.getReceiptSerial(); @@ -92,16 +113,19 @@ public class DonationController { final long receiptLevel = receiptCredentialPresentation.getReceiptLevel(); final String badgeId = badgesConfiguration.getReceiptLevels().get(receiptLevel); if (badgeId == null) { - return CompletableFuture.completedFuture(Response.serverError().entity("server does not recognize the requested receipt level").type(MediaType.TEXT_PLAIN_TYPE).build()); + return CompletableFuture.completedFuture(Response.serverError() + .entity("server does not recognize the requested receipt level") + .type(MediaType.TEXT_PLAIN_TYPE) + .build()); } return redeemedReceiptsManager.put( receiptSerial, receiptExpiration.getEpochSecond(), receiptLevel, auth.getAccount().getUuid()) .thenCompose(receiptMatched -> { if (!receiptMatched) { - - return CompletableFuture.completedFuture( - Response.status(Status.BAD_REQUEST).entity("receipt serial is already redeemed") - .type(MediaType.TEXT_PLAIN_TYPE).build()); + return CompletableFuture.completedFuture(Response.status(Status.BAD_REQUEST) + .entity("receipt serial is already redeemed") + .type(MediaType.TEXT_PLAIN_TYPE) + .build()); } return accountsManager.updateAsync(auth.getAccount(), a -> { @@ -111,7 +135,7 @@ public class DonationController { } }) .thenApply(ignored -> Response.ok().build()); - }); + }); }).thenCompose(Function.identity()); } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/RedeemReceiptRequest.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/RedeemReceiptRequest.java index a17fec553..072e65ed7 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/RedeemReceiptRequest.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/RedeemReceiptRequest.java @@ -7,12 +7,16 @@ package org.whispersystems.textsecuregcm.entities; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; public class RedeemReceiptRequest { + @Schema(description = "Presentation of a ZK receipt encoded in standard padded base64", implementation = String.class) private final byte[] receiptCredentialPresentation; + @Schema(description = "If true, the corresponding badge should be visible on the profile") private final boolean visible; + @Schema(description = "if true, and the new badge is visible, it should be the primary badge on the profile") private final boolean primary; @JsonCreator