diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java index 15551c5be..bfe7034a1 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java @@ -20,6 +20,7 @@ import org.whispersystems.textsecuregcm.configuration.ApnConfiguration; import org.whispersystems.textsecuregcm.configuration.AppConfigConfiguration; import org.whispersystems.textsecuregcm.configuration.AwsAttachmentsConfiguration; import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration; +import org.whispersystems.textsecuregcm.configuration.BoostConfiguration; import org.whispersystems.textsecuregcm.configuration.CdnConfiguration; import org.whispersystems.textsecuregcm.configuration.DatabaseConfiguration; import org.whispersystems.textsecuregcm.configuration.DatadogConfiguration; @@ -319,6 +320,11 @@ public class WhisperServerConfiguration extends Configuration { // TODO: Mark as @NotNull when enabled for production. private SubscriptionConfiguration subscription; + @Valid + @JsonProperty + // TODO: Mark as @NotNull when enabled for production. + private BoostConfiguration boost; + @Valid @NotNull @JsonProperty @@ -552,6 +558,10 @@ public class WhisperServerConfiguration extends Configuration { return subscription; } + public BoostConfiguration getBoost() { + return boost; + } + public ReportMessageConfiguration getReportMessageConfiguration() { return reportMessage; } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index 668fc0d43..af4adcfdd 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -640,9 +640,9 @@ public class WhisperServerService extends Application> currencies; + + @JsonCreator + public BoostConfiguration( + @JsonProperty("currencies") final Map> currencies) { + this.currencies = currencies; + } + + @Valid + @NotNull + public Map<@NotEmpty String, @Valid @ExactlySize(6) List<@DecimalMin("0.01") @NotNull BigDecimal>> getCurrencies() { + return currencies; + } +} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java index 2563e246c..5350186bd 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SubscriptionController.java @@ -29,6 +29,7 @@ import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.function.Function; import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.crypto.Mac; @@ -62,6 +63,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; import org.whispersystems.textsecuregcm.badges.BadgeTranslator; +import org.whispersystems.textsecuregcm.configuration.BoostConfiguration; import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration; import org.whispersystems.textsecuregcm.configuration.SubscriptionLevelConfiguration; import org.whispersystems.textsecuregcm.configuration.SubscriptionPriceConfiguration; @@ -78,7 +80,8 @@ public class SubscriptionController { private static final Logger logger = LoggerFactory.getLogger(SubscriptionController.class); private final Clock clock; - private final SubscriptionConfiguration config; + private final SubscriptionConfiguration subscriptionConfiguration; + private final BoostConfiguration boostConfiguration; private final SubscriptionManager subscriptionManager; private final StripeManager stripeManager; private final ServerZkReceiptOperations zkReceiptOperations; @@ -87,14 +90,16 @@ public class SubscriptionController { public SubscriptionController( @Nonnull Clock clock, - @Nonnull SubscriptionConfiguration config, + @Nonnull SubscriptionConfiguration subscriptionConfiguration, + @Nonnull final BoostConfiguration boostConfiguration, @Nonnull SubscriptionManager subscriptionManager, @Nonnull StripeManager stripeManager, @Nonnull ServerZkReceiptOperations zkReceiptOperations, @Nonnull IssuedReceiptsManager issuedReceiptsManager, @Nonnull BadgeTranslator badgeTranslator) { this.clock = Objects.requireNonNull(clock); - this.config = Objects.requireNonNull(config); + this.subscriptionConfiguration = Objects.requireNonNull(subscriptionConfiguration); + this.boostConfiguration = Objects.requireNonNull(boostConfiguration); this.subscriptionManager = Objects.requireNonNull(subscriptionManager); this.stripeManager = Objects.requireNonNull(stripeManager); this.zkReceiptOperations = Objects.requireNonNull(zkReceiptOperations); @@ -288,7 +293,7 @@ public class SubscriptionController { return subscriptionManager.get(requestData.subscriberUser, requestData.hmac) .thenApply(this::requireRecordFromGetResult) .thenCompose(record -> { - SubscriptionLevelConfiguration levelConfiguration = config.getLevels().get(level); + SubscriptionLevelConfiguration levelConfiguration = subscriptionConfiguration.getLevels().get(level); if (levelConfiguration == null) { throw new BadRequestException(Response.status(Status.BAD_REQUEST) .entity(new SetSubscriptionLevelErrorResponse(List.of( @@ -307,12 +312,14 @@ public class SubscriptionController { } if (record.subscriptionId == null) { - long lastSubscriptionCreatedAt = record.subscriptionCreatedAt != null ? record.subscriptionCreatedAt.getEpochSecond() : 0; + long lastSubscriptionCreatedAt = + record.subscriptionCreatedAt != null ? record.subscriptionCreatedAt.getEpochSecond() : 0; // we don't have one yet so create it and then record the subscription id // // this relies on stripe's idempotency key to avoid creating more than one subscription if the client // retries this request - return stripeManager.createSubscription(record.customerId, priceConfiguration.getId(), level, lastSubscriptionCreatedAt) + return stripeManager.createSubscription(record.customerId, priceConfiguration.getId(), level, + lastSubscriptionCreatedAt) .thenCompose(subscription -> subscriptionManager.subscriptionCreated( requestData.subscriberUser, subscription.getId(), requestData.now, level) .thenApply(unused -> subscription)); @@ -381,7 +388,7 @@ public class SubscriptionController { return CompletableFuture.supplyAsync(() -> { List acceptableLanguages = getAcceptableLanguagesForRequest(containerRequestContext); GetLevelsResponse getLevelsResponse = new GetLevelsResponse( - config.getLevels().entrySet().stream().collect(Collectors.toMap(Entry::getKey, + subscriptionConfiguration.getLevels().entrySet().stream().collect(Collectors.toMap(Entry::getKey, entry -> new GetLevelsResponse.Level( badgeTranslator.translate(acceptableLanguages, entry.getValue().getBadge()), entry.getValue().getPrices().entrySet().stream().collect( @@ -391,6 +398,17 @@ public class SubscriptionController { }); } + @Timed + @GET + @Path("/boost/amounts") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public CompletableFuture getBoostAmounts() { + return CompletableFuture.supplyAsync(() -> Response.ok( + boostConfiguration.getCurrencies().entrySet().stream().collect( + Collectors.toMap(entry -> entry.getKey().toUpperCase(Locale.ROOT), Function.identity()))).build()); + } + public static class GetSubscriptionInformationResponse { public static class Subscription { @@ -619,7 +637,7 @@ public class SubscriptionController { InvoiceLineItem subscriptionLineItem = subscriptionLineItems.stream().findAny().get(); return stripeManager.getProductForPrice(subscriptionLineItem.getPrice().getId()).thenApply(product -> new Receipt( Instant.ofEpochSecond(subscriptionLineItem.getPeriod().getEnd()) - .plus(config.getBadgeGracePeriod()) + .plus(subscriptionConfiguration.getBadgeGracePeriod()) .truncatedTo(ChronoUnit.DAYS) .plus(1, ChronoUnit.DAYS), stripeManager.getLevelForProduct(product), diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/util/ExactlySize.java b/service/src/main/java/org/whispersystems/textsecuregcm/util/ExactlySize.java index de2d68caa..d77a32ebd 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/util/ExactlySize.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/util/ExactlySize.java @@ -6,9 +6,11 @@ package org.whispersystems.textsecuregcm.util; import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.CONSTRUCTOR; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Documented; @@ -17,7 +19,7 @@ import java.lang.annotation.Target; import javax.validation.Constraint; import javax.validation.Payload; -@Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE }) +@Target({ FIELD, METHOD, CONSTRUCTOR, PARAMETER, ANNOTATION_TYPE, TYPE_USE }) @Retention(RUNTIME) @Constraint(validatedBy = { ExactlySizeValidatorForString.class, diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SubscriptionControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SubscriptionControllerTest.java index 5ef7abe8b..bb0f7b16f 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SubscriptionControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SubscriptionControllerTest.java @@ -29,6 +29,7 @@ import org.signal.zkgroup.receipts.ServerZkReceiptOperations; import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount; import org.whispersystems.textsecuregcm.badges.BadgeTranslator; +import org.whispersystems.textsecuregcm.configuration.BoostConfiguration; import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration; import org.whispersystems.textsecuregcm.configuration.SubscriptionLevelConfiguration; import org.whispersystems.textsecuregcm.configuration.SubscriptionPriceConfiguration; @@ -45,14 +46,15 @@ class SubscriptionControllerTest { private static final Clock CLOCK = mock(Clock.class); private static final SubscriptionConfiguration SUBSCRIPTION_CONFIG = mock(SubscriptionConfiguration.class); + private static final BoostConfiguration BOOST_CONFIG = mock(BoostConfiguration.class); private static final SubscriptionManager SUBSCRIPTION_MANAGER = mock(SubscriptionManager.class); private static final StripeManager STRIPE_MANAGER = mock(StripeManager.class); private static final ServerZkReceiptOperations ZK_OPS = mock(ServerZkReceiptOperations.class); private static final IssuedReceiptsManager ISSUED_RECEIPTS_MANAGER = mock(IssuedReceiptsManager.class); private static final BadgeTranslator BADGE_TRANSLATOR = mock(BadgeTranslator.class); private static final SubscriptionController SUBSCRIPTION_CONTROLLER = new SubscriptionController( - CLOCK, SUBSCRIPTION_CONFIG, SUBSCRIPTION_MANAGER, STRIPE_MANAGER, ZK_OPS, ISSUED_RECEIPTS_MANAGER, - BADGE_TRANSLATOR); + CLOCK, SUBSCRIPTION_CONFIG, BOOST_CONFIG, SUBSCRIPTION_MANAGER, STRIPE_MANAGER, ZK_OPS, + ISSUED_RECEIPTS_MANAGER, BADGE_TRANSLATOR); private static final ResourceExtension RESOURCE_EXTENSION = ResourceExtension.builder() .addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE) .addProvider(AuthHelper.getAuthFilter())