Calculate onetime badge expiration from payment success timestamp
This commit is contained in:
parent
1167d0ac2e
commit
3548c3df15
|
@ -102,6 +102,8 @@ dynamoDbTables:
|
|||
messages:
|
||||
tableName: Example_Messages
|
||||
expiration: P30D # Duration of time until rows expire
|
||||
onetimeDonations:
|
||||
tableName: Example_OnetimeDonations
|
||||
phoneNumberIdentifiers:
|
||||
tableName: Example_PhoneNumberIdentifiers
|
||||
profiles:
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
}
|
|
@ -68,6 +68,7 @@ import org.whispersystems.textsecuregcm.entities.BadgeSvg;
|
|||
import org.whispersystems.textsecuregcm.mappers.CompletionExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.SubscriptionProcessorExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.OneTimeDonationsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.SubscriptionManager;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.BankMandateTranslator;
|
||||
import org.whispersystems.textsecuregcm.subscriptions.BraintreeManager;
|
||||
|
@ -98,12 +99,13 @@ class SubscriptionControllerTest {
|
|||
private static final PaymentIntent PAYMENT_INTENT = mock(PaymentIntent.class);
|
||||
private static final ServerZkReceiptOperations ZK_OPS = mock(ServerZkReceiptOperations.class);
|
||||
private static final IssuedReceiptsManager ISSUED_RECEIPTS_MANAGER = mock(IssuedReceiptsManager.class);
|
||||
private static final OneTimeDonationsManager ONE_TIME_DONATIONS_MANAGER = mock(OneTimeDonationsManager.class);
|
||||
private static final BadgeTranslator BADGE_TRANSLATOR = mock(BadgeTranslator.class);
|
||||
private static final LevelTranslator LEVEL_TRANSLATOR = mock(LevelTranslator.class);
|
||||
private static final BankMandateTranslator BANK_MANDATE_TRANSLATOR = mock(BankMandateTranslator.class);
|
||||
private static final SubscriptionController SUBSCRIPTION_CONTROLLER = new SubscriptionController(
|
||||
CLOCK, SUBSCRIPTION_CONFIG, ONETIME_CONFIG, SUBSCRIPTION_MANAGER, STRIPE_MANAGER, BRAINTREE_MANAGER, ZK_OPS,
|
||||
ISSUED_RECEIPTS_MANAGER, BADGE_TRANSLATOR, LEVEL_TRANSLATOR, BANK_MANDATE_TRANSLATOR);
|
||||
ISSUED_RECEIPTS_MANAGER, ONE_TIME_DONATIONS_MANAGER, BADGE_TRANSLATOR, LEVEL_TRANSLATOR, BANK_MANDATE_TRANSLATOR);
|
||||
private static final ResourceExtension RESOURCE_EXTENSION = ResourceExtension.builder()
|
||||
.addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE)
|
||||
.addProvider(AuthHelper.getAuthFilter())
|
||||
|
|
|
@ -242,6 +242,15 @@ public final class DynamoDbExtensionSchema {
|
|||
.projection(Projection.builder().projectionType(ProjectionType.KEYS_ONLY).build())
|
||||
.build())),
|
||||
|
||||
ONETIME_DONATIONS("onetime_donations_test",
|
||||
OneTimeDonationsManager.KEY_PAYMENT_INTENT_ID,
|
||||
null,
|
||||
List.of(AttributeDefinition.builder()
|
||||
.attributeName(OneTimeDonationsManager.KEY_PAYMENT_INTENT_ID)
|
||||
.attributeType(ScalarAttributeType.S)
|
||||
.build()),
|
||||
List.of(), List.of()),
|
||||
|
||||
PROFILES("profiles_test",
|
||||
Profiles.KEY_ACCOUNT_UUID,
|
||||
Profiles.ATTR_VERSION,
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.time.Instant;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
|
||||
public class OnetimeDonationsManagerTest {
|
||||
@RegisterExtension
|
||||
static final DynamoDbExtension DYNAMO_DB_EXTENSION = new DynamoDbExtension(DynamoDbExtensionSchema.Tables.ONETIME_DONATIONS);
|
||||
private OneTimeDonationsManager oneTimeDonationsManager;
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
oneTimeDonationsManager = new OneTimeDonationsManager(
|
||||
DynamoDbExtensionSchema.Tables.ONETIME_DONATIONS.tableName(),
|
||||
DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetPaidAtTimestamp() {
|
||||
final String validPaymentIntentId = "abc";
|
||||
final Instant paidAt = Instant.ofEpochSecond(1_000_000);
|
||||
final Instant fallBackTimestamp = Instant.ofEpochSecond(2_000_000);
|
||||
oneTimeDonationsManager.putPaidAt(validPaymentIntentId, paidAt).join();
|
||||
|
||||
assertThat(oneTimeDonationsManager.getPaidAt(validPaymentIntentId, fallBackTimestamp).join()).isEqualTo(paidAt);
|
||||
assertThat(oneTimeDonationsManager.getPaidAt("invalidPaymentId", fallBackTimestamp).join()).isEqualTo(fallBackTimestamp);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue