Add method to retrieve receipt credentials for a boost payment
This commit is contained in:
parent
d27ec6fe8d
commit
090d722b61
|
@ -8,6 +8,7 @@ package org.whispersystems.textsecuregcm.configuration;
|
||||||
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 java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.time.Duration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
|
@ -18,14 +19,29 @@ import org.whispersystems.textsecuregcm.util.ExactlySize;
|
||||||
|
|
||||||
public class BoostConfiguration {
|
public class BoostConfiguration {
|
||||||
|
|
||||||
|
private final long level;
|
||||||
|
private final Duration expiration;
|
||||||
private final Map<String, List<BigDecimal>> currencies;
|
private final Map<String, List<BigDecimal>> currencies;
|
||||||
|
|
||||||
@JsonCreator
|
@JsonCreator
|
||||||
public BoostConfiguration(
|
public BoostConfiguration(
|
||||||
|
@JsonProperty("level") long level,
|
||||||
|
@JsonProperty("expiration") Duration expiration,
|
||||||
@JsonProperty("currencies") final Map<String, List<BigDecimal>> currencies) {
|
@JsonProperty("currencies") final Map<String, List<BigDecimal>> currencies) {
|
||||||
|
this.level = level;
|
||||||
|
this.expiration = expiration;
|
||||||
this.currencies = currencies;
|
this.currencies = currencies;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getLevel() {
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public Duration getExpiration() {
|
||||||
|
return expiration;
|
||||||
|
}
|
||||||
|
|
||||||
@Valid
|
@Valid
|
||||||
@NotNull
|
@NotNull
|
||||||
public Map<@NotEmpty String, @Valid @ExactlySize(6) List<@DecimalMin("0.01") @NotNull BigDecimal>> getCurrencies() {
|
public Map<@NotEmpty String, @Valid @ExactlySize(6) List<@DecimalMin("0.01") @NotNull BigDecimal>> getCurrencies() {
|
||||||
|
|
|
@ -50,6 +50,7 @@ import javax.ws.rs.Path;
|
||||||
import javax.ws.rs.PathParam;
|
import javax.ws.rs.PathParam;
|
||||||
import javax.ws.rs.ProcessingException;
|
import javax.ws.rs.ProcessingException;
|
||||||
import javax.ws.rs.Produces;
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.WebApplicationException;
|
||||||
import javax.ws.rs.container.ContainerRequestContext;
|
import javax.ws.rs.container.ContainerRequestContext;
|
||||||
import javax.ws.rs.core.Context;
|
import javax.ws.rs.core.Context;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
|
@ -460,6 +461,82 @@ public class SubscriptionController {
|
||||||
.thenApply(paymentIntent -> Response.ok(new CreateBoostResponse(paymentIntent.getClientSecret())).build());
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPaymentIntentId() {
|
||||||
|
return paymentIntentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getReceiptCredentialRequest() {
|
||||||
|
return receiptCredentialRequest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class CreateBoostReceiptCredentialsResponse {
|
||||||
|
|
||||||
|
private final byte[] receiptCredentialResponse;
|
||||||
|
|
||||||
|
@JsonCreator
|
||||||
|
public CreateBoostReceiptCredentialsResponse(
|
||||||
|
@JsonProperty("receiptCredentialResponse") byte[] receiptCredentialResponse) {
|
||||||
|
this.receiptCredentialResponse = receiptCredentialResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getReceiptCredentialResponse() {
|
||||||
|
return receiptCredentialResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Timed
|
||||||
|
@POST
|
||||||
|
@Path("/boost/receipt_credentials")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public CompletableFuture<Response> createBoostReceiptCredentials(CreateBoostReceiptCredentialsRequest request) {
|
||||||
|
return stripeManager.getPaymentIntent(request.getPaymentIntentId())
|
||||||
|
.thenCompose(paymentIntent -> {
|
||||||
|
if (paymentIntent == null) {
|
||||||
|
throw new WebApplicationException(Status.NO_CONTENT);
|
||||||
|
}
|
||||||
|
if (!"succeeded".equalsIgnoreCase(paymentIntent.getStatus())) {
|
||||||
|
throw new WebApplicationException(Status.NO_CONTENT);
|
||||||
|
}
|
||||||
|
ReceiptCredentialRequest receiptCredentialRequest;
|
||||||
|
try {
|
||||||
|
receiptCredentialRequest = new ReceiptCredentialRequest(request.getReceiptCredentialRequest());
|
||||||
|
} catch (InvalidInputException e) {
|
||||||
|
throw new BadRequestException("invalid receipt credential request", e);
|
||||||
|
}
|
||||||
|
return issuedReceiptsManager.recordIssuance(paymentIntent.getId(), receiptCredentialRequest, clock.instant())
|
||||||
|
.thenApply(unused -> {
|
||||||
|
Instant expiration = Instant.ofEpochSecond(paymentIntent.getCreated())
|
||||||
|
.plus(boostConfiguration.getExpiration())
|
||||||
|
.truncatedTo(ChronoUnit.DAYS)
|
||||||
|
.plus(1, ChronoUnit.DAYS);
|
||||||
|
ReceiptCredentialResponse receiptCredentialResponse;
|
||||||
|
try {
|
||||||
|
receiptCredentialResponse = zkReceiptOperations.issueReceiptCredential(
|
||||||
|
receiptCredentialRequest, expiration.getEpochSecond(), boostConfiguration.getLevel());
|
||||||
|
} catch (VerificationFailedException e) {
|
||||||
|
throw new BadRequestException("receipt credential request failed verification", e);
|
||||||
|
}
|
||||||
|
return Response.ok(new CreateBoostReceiptCredentialsResponse(receiptCredentialResponse.serialize()))
|
||||||
|
.build();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public static class GetSubscriptionInformationResponse {
|
public static class GetSubscriptionInformationResponse {
|
||||||
|
|
||||||
public static class Subscription {
|
public static class Subscription {
|
||||||
|
|
|
@ -33,7 +33,7 @@ import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
|
||||||
|
|
||||||
public class IssuedReceiptsManager {
|
public class IssuedReceiptsManager {
|
||||||
|
|
||||||
public static final String KEY_INVOICE_LINE_ITEM_ID = "A"; // S (HashKey)
|
public static final String KEY_STRIPE_ID = "A"; // S (HashKey)
|
||||||
public static final String KEY_ISSUED_RECEIPT_TAG = "B"; // B
|
public static final String KEY_ISSUED_RECEIPT_TAG = "B"; // B
|
||||||
public static final String KEY_EXPIRATION = "E"; // N
|
public static final String KEY_EXPIRATION = "E"; // N
|
||||||
|
|
||||||
|
@ -54,27 +54,29 @@ public class IssuedReceiptsManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a future that completes normally if either this invoice line item was never issued a receipt credential
|
* Returns a future that completes normally if either this stripe item was never issued a receipt credential
|
||||||
* previously OR if it was issued a receipt credential previously for the exact same receipt credential request
|
* previously OR if it was issued a receipt credential previously for the exact same receipt credential request
|
||||||
* enabling clients to retry in case they missed the original response.
|
* enabling clients to retry in case they missed the original response.
|
||||||
*
|
*
|
||||||
* If this invoice line item id has already been used to issue another receipt, throws a 409 conflict web application
|
* If this stripe item has already been used to issue another receipt, throws a 409 conflict web application
|
||||||
* exception.
|
* exception.
|
||||||
|
*
|
||||||
|
* Stripe item is expected to refer to an invoice line item (subscriptions) or a payment intent (one-time).
|
||||||
*/
|
*/
|
||||||
public CompletableFuture<Void> recordIssuance(
|
public CompletableFuture<Void> recordIssuance(
|
||||||
String invoiceLineItemId,
|
String stripeId,
|
||||||
ReceiptCredentialRequest request,
|
ReceiptCredentialRequest request,
|
||||||
Instant now) {
|
Instant now) {
|
||||||
UpdateItemRequest updateItemRequest = UpdateItemRequest.builder()
|
UpdateItemRequest updateItemRequest = UpdateItemRequest.builder()
|
||||||
.tableName(table)
|
.tableName(table)
|
||||||
.key(Map.of(KEY_INVOICE_LINE_ITEM_ID, s(invoiceLineItemId)))
|
.key(Map.of(KEY_STRIPE_ID, s(stripeId)))
|
||||||
.conditionExpression("attribute_not_exists(#key) OR #tag = :tag")
|
.conditionExpression("attribute_not_exists(#key) OR #tag = :tag")
|
||||||
.returnValues(ReturnValue.NONE)
|
.returnValues(ReturnValue.NONE)
|
||||||
.updateExpression("SET "
|
.updateExpression("SET "
|
||||||
+ "#tag = if_not_exists(#tag, :tag), "
|
+ "#tag = if_not_exists(#tag, :tag), "
|
||||||
+ "#exp = if_not_exists(#exp, :exp)")
|
+ "#exp = if_not_exists(#exp, :exp)")
|
||||||
.expressionAttributeNames(Map.of(
|
.expressionAttributeNames(Map.of(
|
||||||
"#key", KEY_INVOICE_LINE_ITEM_ID,
|
"#key", KEY_STRIPE_ID,
|
||||||
"#tag", KEY_ISSUED_RECEIPT_TAG,
|
"#tag", KEY_ISSUED_RECEIPT_TAG,
|
||||||
"#exp", KEY_EXPIRATION))
|
"#exp", KEY_EXPIRATION))
|
||||||
.expressionAttributeValues(Map.of(
|
.expressionAttributeValues(Map.of(
|
||||||
|
|
|
@ -157,6 +157,20 @@ public class StripeManager {
|
||||||
}, executor);
|
}, executor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CompletableFuture<PaymentIntent> getPaymentIntent(String paymentIntentId) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
try {
|
||||||
|
return PaymentIntent.retrieve(paymentIntentId, commonOptions());
|
||||||
|
} catch (StripeException e) {
|
||||||
|
if (e.getStatusCode() == 404) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
throw new CompletionException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, executor);
|
||||||
|
}
|
||||||
|
|
||||||
public CompletableFuture<Subscription> createSubscription(String customerId, String priceId, long level, long lastSubscriptionCreatedAt) {
|
public CompletableFuture<Subscription> createSubscription(String customerId, String priceId, long level, long lastSubscriptionCreatedAt) {
|
||||||
return CompletableFuture.supplyAsync(() -> {
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
SubscriptionCreateParams params = SubscriptionCreateParams.builder()
|
SubscriptionCreateParams params = SubscriptionCreateParams.builder()
|
||||||
|
|
|
@ -31,9 +31,9 @@ class IssuedReceiptsManagerTest {
|
||||||
@RegisterExtension
|
@RegisterExtension
|
||||||
static DynamoDbExtension dynamoDbExtension = DynamoDbExtension.builder()
|
static DynamoDbExtension dynamoDbExtension = DynamoDbExtension.builder()
|
||||||
.tableName(ISSUED_RECEIPTS_TABLE_NAME)
|
.tableName(ISSUED_RECEIPTS_TABLE_NAME)
|
||||||
.hashKey(IssuedReceiptsManager.KEY_INVOICE_LINE_ITEM_ID)
|
.hashKey(IssuedReceiptsManager.KEY_STRIPE_ID)
|
||||||
.attributeDefinition(AttributeDefinition.builder()
|
.attributeDefinition(AttributeDefinition.builder()
|
||||||
.attributeName(IssuedReceiptsManager.KEY_INVOICE_LINE_ITEM_ID)
|
.attributeName(IssuedReceiptsManager.KEY_STRIPE_ID)
|
||||||
.attributeType(ScalarAttributeType.S)
|
.attributeType(ScalarAttributeType.S)
|
||||||
.build())
|
.build())
|
||||||
.build();
|
.build();
|
||||||
|
|
Loading…
Reference in New Issue