Calculate onetime badge expiration from payment success timestamp

This commit is contained in:
Katherine
2023-12-14 15:39:46 -05:00
committed by GitHub
parent 1167d0ac2e
commit 3548c3df15
8 changed files with 138 additions and 5 deletions

View File

@@ -187,6 +187,7 @@ import org.whispersystems.textsecuregcm.storage.KeysManager;
import org.whispersystems.textsecuregcm.storage.MessagesCache;
import org.whispersystems.textsecuregcm.storage.MessagesDynamoDb;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.storage.OneTimeDonationsManager;
import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers;
import org.whispersystems.textsecuregcm.storage.Profiles;
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
@@ -547,6 +548,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
config.getDynamoDbTables().getIssuedReceipts().getExpiration(),
dynamoDbAsyncClient,
config.getDynamoDbTables().getIssuedReceipts().getGenerator());
OneTimeDonationsManager oneTimeDonationsManager = new OneTimeDonationsManager(
config.getDynamoDbTables().getOnetimeDonations().getTableName(), dynamoDbAsyncClient);
RedeemedReceiptsManager redeemedReceiptsManager = new RedeemedReceiptsManager(clock,
config.getDynamoDbTables().getRedeemedReceipts().getTableName(),
dynamoDbAsyncClient,
@@ -832,8 +835,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
);
if (config.getSubscription() != null && config.getOneTimeDonations() != null) {
commonControllers.add(new SubscriptionController(clock, config.getSubscription(), config.getOneTimeDonations(),
subscriptionManager, stripeManager, braintreeManager, zkReceiptOperations, issuedReceiptsManager, profileBadgeConverter,
resourceBundleLevelTranslator, bankMandateTranslator));
subscriptionManager, stripeManager, braintreeManager, zkReceiptOperations, issuedReceiptsManager, oneTimeDonationsManager,
profileBadgeConverter, resourceBundleLevelTranslator, bankMandateTranslator));
}
for (Object controller : commonControllers) {

View File

@@ -59,6 +59,7 @@ public class DynamoDbTables {
private final Table kemKeys;
private final Table kemLastResortKeys;
private final TableWithExpiration messages;
private final Table onetimeDonations;
private final Table phoneNumberIdentifiers;
private final Table profiles;
private final Table pushChallenge;
@@ -82,6 +83,7 @@ public class DynamoDbTables {
@JsonProperty("pqKeys") final Table kemKeys,
@JsonProperty("pqLastResortKeys") final Table kemLastResortKeys,
@JsonProperty("messages") final TableWithExpiration messages,
@JsonProperty("onetimeDonations") final Table onetimeDonations,
@JsonProperty("phoneNumberIdentifiers") final Table phoneNumberIdentifiers,
@JsonProperty("profiles") final Table profiles,
@JsonProperty("pushChallenge") final Table pushChallenge,
@@ -104,6 +106,7 @@ public class DynamoDbTables {
this.kemKeys = kemKeys;
this.kemLastResortKeys = kemLastResortKeys;
this.messages = messages;
this.onetimeDonations = onetimeDonations;
this.phoneNumberIdentifiers = phoneNumberIdentifiers;
this.profiles = profiles;
this.pushChallenge = pushChallenge;
@@ -187,6 +190,12 @@ public class DynamoDbTables {
return messages;
}
@NotNull
@Valid
public Table getOnetimeDonations() {
return onetimeDonations;
}
@NotNull
@Valid
public Table getPhoneNumberIdentifiers() {

View File

@@ -85,6 +85,7 @@ import org.whispersystems.textsecuregcm.entities.PurchasableBadge;
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager;
import org.whispersystems.textsecuregcm.storage.OneTimeDonationsManager;
import org.whispersystems.textsecuregcm.storage.SubscriptionManager;
import org.whispersystems.textsecuregcm.storage.SubscriptionManager.GetResult;
import org.whispersystems.textsecuregcm.subscriptions.BankMandateTranslator;
@@ -117,6 +118,7 @@ public class SubscriptionController {
private final BraintreeManager braintreeManager;
private final ServerZkReceiptOperations zkReceiptOperations;
private final IssuedReceiptsManager issuedReceiptsManager;
private final OneTimeDonationsManager oneTimeDonationsManager;
private final BadgeTranslator badgeTranslator;
private final LevelTranslator levelTranslator;
private final BankMandateTranslator bankMandateTranslator;
@@ -137,6 +139,7 @@ public class SubscriptionController {
@Nonnull BraintreeManager braintreeManager,
@Nonnull ServerZkReceiptOperations zkReceiptOperations,
@Nonnull IssuedReceiptsManager issuedReceiptsManager,
@Nonnull OneTimeDonationsManager oneTimeDonationsManager,
@Nonnull BadgeTranslator badgeTranslator,
@Nonnull LevelTranslator levelTranslator,
@Nonnull BankMandateTranslator bankMandateTranslator) {
@@ -148,6 +151,7 @@ public class SubscriptionController {
this.braintreeManager = Objects.requireNonNull(braintreeManager);
this.zkReceiptOperations = Objects.requireNonNull(zkReceiptOperations);
this.issuedReceiptsManager = Objects.requireNonNull(issuedReceiptsManager);
this.oneTimeDonationsManager = Objects.requireNonNull(oneTimeDonationsManager);
this.badgeTranslator = Objects.requireNonNull(badgeTranslator);
this.levelTranslator = Objects.requireNonNull(levelTranslator);
this.bankMandateTranslator = Objects.requireNonNull(bankMandateTranslator);
@@ -852,8 +856,9 @@ public class SubscriptionController {
final long finalLevel = level;
return issuedReceiptsManager.recordIssuance(paymentDetails.id(), manager.getProcessor(),
receiptCredentialRequest, clock.instant())
.thenApply(unused -> {
Instant expiration = paymentDetails.created()
.thenCompose(unused -> oneTimeDonationsManager.getPaidAt(paymentDetails.id(), paymentDetails.created()))
.thenApply(paidAt -> {
Instant expiration = paidAt
.plus(levelExpiration)
.truncatedTo(ChronoUnit.DAYS)
.plus(1, ChronoUnit.DAYS);

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage;
import static com.codahale.metrics.MetricRegistry.name;
import com.google.common.annotations.VisibleForTesting;
import io.micrometer.core.instrument.Metrics;
import java.time.Instant;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import javax.annotation.Nonnull;
import org.whispersystems.textsecuregcm.util.AttributeValues;
import org.whispersystems.textsecuregcm.util.Util;
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
public class OneTimeDonationsManager {
public static final String KEY_PAYMENT_INTENT_ID = "P"; // S
public static final String ATTR_PAID_AT = "A"; // N
private static final String ONETIME_DONATION_NOT_FOUND_COUNTER_NAME = name(OneTimeDonationsManager.class, "onetimeDonationNotFound");
private final String table;
private final DynamoDbAsyncClient dynamoDbAsyncClient;
public OneTimeDonationsManager(
@Nonnull String table,
@Nonnull DynamoDbAsyncClient dynamoDbAsyncClient) {
this.table = Objects.requireNonNull(table);
this.dynamoDbAsyncClient = Objects.requireNonNull(dynamoDbAsyncClient);
}
public CompletableFuture<Instant> getPaidAt(final String paymentIntentId, final Instant fallbackTimestamp) {
final GetItemRequest getItemRequest = GetItemRequest.builder()
.consistentRead(Boolean.TRUE)
.tableName(table)
.key(Map.of(KEY_PAYMENT_INTENT_ID, AttributeValues.fromString(paymentIntentId)))
.projectionExpression(ATTR_PAID_AT)
.build();
return dynamoDbAsyncClient.getItem(getItemRequest).thenApply(getItemResponse -> {
if (!getItemResponse.hasItem()) {
Metrics.counter(ONETIME_DONATION_NOT_FOUND_COUNTER_NAME).increment();
return fallbackTimestamp;
}
return Instant.ofEpochSecond(AttributeValues.getLong(getItemResponse.item(), ATTR_PAID_AT, fallbackTimestamp.getEpochSecond()));
});
}
@VisibleForTesting
CompletableFuture<Void> putPaidAt(final String paymentIntentId, final Instant paidAt) {
return dynamoDbAsyncClient.putItem(PutItemRequest.builder()
.tableName(table)
.item(Map.of(
KEY_PAYMENT_INTENT_ID, AttributeValues.fromString(paymentIntentId),
ATTR_PAID_AT, AttributeValues.fromLong(paidAt.getEpochSecond())))
.build())
.thenRun(Util.NOOP);
}
}