Add docs to /v1/donations/redeem-receipt

This commit is contained in:
Ravi Khadiwala 2025-06-12 15:54:26 -05:00 committed by ravi-signal
parent 9a1da23bdb
commit 626a7fdad7
2 changed files with 38 additions and 10 deletions

View File

@ -6,6 +6,8 @@
package org.whispersystems.textsecuregcm.controllers; package org.whispersystems.textsecuregcm.controllers;
import io.dropwizard.auth.Auth; 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 io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
@ -70,21 +72,40 @@ public class DonationController {
@Path("/redeem-receipt") @Path("/redeem-receipt")
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) @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<Response> redeemReceipt( public CompletionStage<Response> redeemReceipt(
@Mutable @Auth final AuthenticatedDevice auth, @Mutable @Auth final AuthenticatedDevice auth,
@NotNull @Valid final RedeemReceiptRequest request) { @NotNull @Valid final RedeemReceiptRequest request) {
return CompletableFuture.supplyAsync(() -> { return CompletableFuture.supplyAsync(() -> {
ReceiptCredentialPresentation receiptCredentialPresentation; ReceiptCredentialPresentation receiptCredentialPresentation;
try { try {
receiptCredentialPresentation = receiptCredentialPresentationFactory.build( receiptCredentialPresentation = receiptCredentialPresentationFactory
request.getReceiptCredentialPresentation()); .build(request.getReceiptCredentialPresentation());
} catch (InvalidInputException e) { } 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 { try {
serverZkReceiptOperations.verifyReceiptCredentialPresentation(receiptCredentialPresentation); serverZkReceiptOperations.verifyReceiptCredentialPresentation(receiptCredentialPresentation);
} catch (VerificationFailedException e) { } 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(); final ReceiptSerial receiptSerial = receiptCredentialPresentation.getReceiptSerial();
@ -92,16 +113,19 @@ public class DonationController {
final long receiptLevel = receiptCredentialPresentation.getReceiptLevel(); final long receiptLevel = receiptCredentialPresentation.getReceiptLevel();
final String badgeId = badgesConfiguration.getReceiptLevels().get(receiptLevel); final String badgeId = badgesConfiguration.getReceiptLevels().get(receiptLevel);
if (badgeId == null) { 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( return redeemedReceiptsManager.put(
receiptSerial, receiptExpiration.getEpochSecond(), receiptLevel, auth.getAccount().getUuid()) receiptSerial, receiptExpiration.getEpochSecond(), receiptLevel, auth.getAccount().getUuid())
.thenCompose(receiptMatched -> { .thenCompose(receiptMatched -> {
if (!receiptMatched) { if (!receiptMatched) {
return CompletableFuture.completedFuture(Response.status(Status.BAD_REQUEST)
return CompletableFuture.completedFuture( .entity("receipt serial is already redeemed")
Response.status(Status.BAD_REQUEST).entity("receipt serial is already redeemed") .type(MediaType.TEXT_PLAIN_TYPE)
.type(MediaType.TEXT_PLAIN_TYPE).build()); .build());
} }
return accountsManager.updateAsync(auth.getAccount(), a -> { return accountsManager.updateAsync(auth.getAccount(), a -> {
@ -111,7 +135,7 @@ public class DonationController {
} }
}) })
.thenApply(ignored -> Response.ok().build()); .thenApply(ignored -> Response.ok().build());
}); });
}).thenCompose(Function.identity()); }).thenCompose(Function.identity());
} }

View File

@ -7,12 +7,16 @@ package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotEmpty;
public class RedeemReceiptRequest { public class RedeemReceiptRequest {
@Schema(description = "Presentation of a ZK receipt encoded in standard padded base64", implementation = String.class)
private final byte[] receiptCredentialPresentation; private final byte[] receiptCredentialPresentation;
@Schema(description = "If true, the corresponding badge should be visible on the profile")
private final boolean visible; 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; private final boolean primary;
@JsonCreator @JsonCreator