Add endpoint for fetching boost amounts
This commit is contained in:
parent
3b764bed7a
commit
07cd69ab34
|
@ -20,6 +20,7 @@ import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.AppConfigConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.AppConfigConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.AwsAttachmentsConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.AwsAttachmentsConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.BoostConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.CdnConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.CdnConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.DatabaseConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.DatabaseConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.DatadogConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.DatadogConfiguration;
|
||||||
|
@ -319,6 +320,11 @@ public class WhisperServerConfiguration extends Configuration {
|
||||||
// TODO: Mark as @NotNull when enabled for production.
|
// TODO: Mark as @NotNull when enabled for production.
|
||||||
private SubscriptionConfiguration subscription;
|
private SubscriptionConfiguration subscription;
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@JsonProperty
|
||||||
|
// TODO: Mark as @NotNull when enabled for production.
|
||||||
|
private BoostConfiguration boost;
|
||||||
|
|
||||||
@Valid
|
@Valid
|
||||||
@NotNull
|
@NotNull
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
|
@ -552,6 +558,10 @@ public class WhisperServerConfiguration extends Configuration {
|
||||||
return subscription;
|
return subscription;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BoostConfiguration getBoost() {
|
||||||
|
return boost;
|
||||||
|
}
|
||||||
|
|
||||||
public ReportMessageConfiguration getReportMessageConfiguration() {
|
public ReportMessageConfiguration getReportMessageConfiguration() {
|
||||||
return reportMessage;
|
return reportMessage;
|
||||||
}
|
}
|
||||||
|
|
|
@ -640,9 +640,9 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
config.getCdnConfiguration().getAccessSecret(), config.getCdnConfiguration().getRegion(),
|
config.getCdnConfiguration().getAccessSecret(), config.getCdnConfiguration().getRegion(),
|
||||||
config.getCdnConfiguration().getBucket())
|
config.getCdnConfiguration().getBucket())
|
||||||
);
|
);
|
||||||
if (config.getSubscription() != null) {
|
if (config.getSubscription() != null && config.getBoost() != null) {
|
||||||
commonControllers.add(new SubscriptionController(clock, config.getSubscription(), subscriptionManager,
|
commonControllers.add(new SubscriptionController(clock, config.getSubscription(), config.getBoost(),
|
||||||
stripeManager, zkReceiptOperations, issuedReceiptsManager, profileBadgeConverter));
|
subscriptionManager, stripeManager, zkReceiptOperations, issuedReceiptsManager, profileBadgeConverter));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Object controller : commonControllers) {
|
for (Object controller : commonControllers) {
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.configuration;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import javax.validation.constraints.DecimalMin;
|
||||||
|
import javax.validation.constraints.NotEmpty;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import org.whispersystems.textsecuregcm.util.ExactlySize;
|
||||||
|
|
||||||
|
public class BoostConfiguration {
|
||||||
|
|
||||||
|
private final Map<String, List<BigDecimal>> currencies;
|
||||||
|
|
||||||
|
@JsonCreator
|
||||||
|
public BoostConfiguration(
|
||||||
|
@JsonProperty("currencies") final Map<String, List<BigDecimal>> currencies) {
|
||||||
|
this.currencies = currencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@NotNull
|
||||||
|
public Map<@NotEmpty String, @Valid @ExactlySize(6) List<@DecimalMin("0.01") @NotNull BigDecimal>> getCurrencies() {
|
||||||
|
return currencies;
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,6 +29,7 @@ import java.util.Map.Entry;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.crypto.Mac;
|
import javax.crypto.Mac;
|
||||||
|
@ -62,6 +63,7 @@ import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||||
import org.whispersystems.textsecuregcm.badges.BadgeTranslator;
|
import org.whispersystems.textsecuregcm.badges.BadgeTranslator;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.BoostConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.SubscriptionLevelConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.SubscriptionLevelConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.SubscriptionPriceConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.SubscriptionPriceConfiguration;
|
||||||
|
@ -78,7 +80,8 @@ public class SubscriptionController {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(SubscriptionController.class);
|
private static final Logger logger = LoggerFactory.getLogger(SubscriptionController.class);
|
||||||
|
|
||||||
private final Clock clock;
|
private final Clock clock;
|
||||||
private final SubscriptionConfiguration config;
|
private final SubscriptionConfiguration subscriptionConfiguration;
|
||||||
|
private final BoostConfiguration boostConfiguration;
|
||||||
private final SubscriptionManager subscriptionManager;
|
private final SubscriptionManager subscriptionManager;
|
||||||
private final StripeManager stripeManager;
|
private final StripeManager stripeManager;
|
||||||
private final ServerZkReceiptOperations zkReceiptOperations;
|
private final ServerZkReceiptOperations zkReceiptOperations;
|
||||||
|
@ -87,14 +90,16 @@ public class SubscriptionController {
|
||||||
|
|
||||||
public SubscriptionController(
|
public SubscriptionController(
|
||||||
@Nonnull Clock clock,
|
@Nonnull Clock clock,
|
||||||
@Nonnull SubscriptionConfiguration config,
|
@Nonnull SubscriptionConfiguration subscriptionConfiguration,
|
||||||
|
@Nonnull final BoostConfiguration boostConfiguration,
|
||||||
@Nonnull SubscriptionManager subscriptionManager,
|
@Nonnull SubscriptionManager subscriptionManager,
|
||||||
@Nonnull StripeManager stripeManager,
|
@Nonnull StripeManager stripeManager,
|
||||||
@Nonnull ServerZkReceiptOperations zkReceiptOperations,
|
@Nonnull ServerZkReceiptOperations zkReceiptOperations,
|
||||||
@Nonnull IssuedReceiptsManager issuedReceiptsManager,
|
@Nonnull IssuedReceiptsManager issuedReceiptsManager,
|
||||||
@Nonnull BadgeTranslator badgeTranslator) {
|
@Nonnull BadgeTranslator badgeTranslator) {
|
||||||
this.clock = Objects.requireNonNull(clock);
|
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.subscriptionManager = Objects.requireNonNull(subscriptionManager);
|
||||||
this.stripeManager = Objects.requireNonNull(stripeManager);
|
this.stripeManager = Objects.requireNonNull(stripeManager);
|
||||||
this.zkReceiptOperations = Objects.requireNonNull(zkReceiptOperations);
|
this.zkReceiptOperations = Objects.requireNonNull(zkReceiptOperations);
|
||||||
|
@ -288,7 +293,7 @@ public class SubscriptionController {
|
||||||
return subscriptionManager.get(requestData.subscriberUser, requestData.hmac)
|
return subscriptionManager.get(requestData.subscriberUser, requestData.hmac)
|
||||||
.thenApply(this::requireRecordFromGetResult)
|
.thenApply(this::requireRecordFromGetResult)
|
||||||
.thenCompose(record -> {
|
.thenCompose(record -> {
|
||||||
SubscriptionLevelConfiguration levelConfiguration = config.getLevels().get(level);
|
SubscriptionLevelConfiguration levelConfiguration = subscriptionConfiguration.getLevels().get(level);
|
||||||
if (levelConfiguration == null) {
|
if (levelConfiguration == null) {
|
||||||
throw new BadRequestException(Response.status(Status.BAD_REQUEST)
|
throw new BadRequestException(Response.status(Status.BAD_REQUEST)
|
||||||
.entity(new SetSubscriptionLevelErrorResponse(List.of(
|
.entity(new SetSubscriptionLevelErrorResponse(List.of(
|
||||||
|
@ -307,12 +312,14 @@ public class SubscriptionController {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (record.subscriptionId == null) {
|
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
|
// 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
|
// this relies on stripe's idempotency key to avoid creating more than one subscription if the client
|
||||||
// retries this request
|
// 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(
|
.thenCompose(subscription -> subscriptionManager.subscriptionCreated(
|
||||||
requestData.subscriberUser, subscription.getId(), requestData.now, level)
|
requestData.subscriberUser, subscription.getId(), requestData.now, level)
|
||||||
.thenApply(unused -> subscription));
|
.thenApply(unused -> subscription));
|
||||||
|
@ -381,7 +388,7 @@ public class SubscriptionController {
|
||||||
return CompletableFuture.supplyAsync(() -> {
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
List<Locale> acceptableLanguages = getAcceptableLanguagesForRequest(containerRequestContext);
|
List<Locale> acceptableLanguages = getAcceptableLanguagesForRequest(containerRequestContext);
|
||||||
GetLevelsResponse getLevelsResponse = new GetLevelsResponse(
|
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(
|
entry -> new GetLevelsResponse.Level(
|
||||||
badgeTranslator.translate(acceptableLanguages, entry.getValue().getBadge()),
|
badgeTranslator.translate(acceptableLanguages, entry.getValue().getBadge()),
|
||||||
entry.getValue().getPrices().entrySet().stream().collect(
|
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<Response> 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 GetSubscriptionInformationResponse {
|
||||||
|
|
||||||
public static class Subscription {
|
public static class Subscription {
|
||||||
|
@ -619,7 +637,7 @@ public class SubscriptionController {
|
||||||
InvoiceLineItem subscriptionLineItem = subscriptionLineItems.stream().findAny().get();
|
InvoiceLineItem subscriptionLineItem = subscriptionLineItems.stream().findAny().get();
|
||||||
return stripeManager.getProductForPrice(subscriptionLineItem.getPrice().getId()).thenApply(product -> new Receipt(
|
return stripeManager.getProductForPrice(subscriptionLineItem.getPrice().getId()).thenApply(product -> new Receipt(
|
||||||
Instant.ofEpochSecond(subscriptionLineItem.getPeriod().getEnd())
|
Instant.ofEpochSecond(subscriptionLineItem.getPeriod().getEnd())
|
||||||
.plus(config.getBadgeGracePeriod())
|
.plus(subscriptionConfiguration.getBadgeGracePeriod())
|
||||||
.truncatedTo(ChronoUnit.DAYS)
|
.truncatedTo(ChronoUnit.DAYS)
|
||||||
.plus(1, ChronoUnit.DAYS),
|
.plus(1, ChronoUnit.DAYS),
|
||||||
stripeManager.getLevelForProduct(product),
|
stripeManager.getLevelForProduct(product),
|
||||||
|
|
|
@ -6,9 +6,11 @@
|
||||||
package org.whispersystems.textsecuregcm.util;
|
package org.whispersystems.textsecuregcm.util;
|
||||||
|
|
||||||
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
|
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.FIELD;
|
||||||
import static java.lang.annotation.ElementType.METHOD;
|
import static java.lang.annotation.ElementType.METHOD;
|
||||||
import static java.lang.annotation.ElementType.PARAMETER;
|
import static java.lang.annotation.ElementType.PARAMETER;
|
||||||
|
import static java.lang.annotation.ElementType.TYPE_USE;
|
||||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||||
|
|
||||||
import java.lang.annotation.Documented;
|
import java.lang.annotation.Documented;
|
||||||
|
@ -17,7 +19,7 @@ import java.lang.annotation.Target;
|
||||||
import javax.validation.Constraint;
|
import javax.validation.Constraint;
|
||||||
import javax.validation.Payload;
|
import javax.validation.Payload;
|
||||||
|
|
||||||
@Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE })
|
@Target({ FIELD, METHOD, CONSTRUCTOR, PARAMETER, ANNOTATION_TYPE, TYPE_USE })
|
||||||
@Retention(RUNTIME)
|
@Retention(RUNTIME)
|
||||||
@Constraint(validatedBy = {
|
@Constraint(validatedBy = {
|
||||||
ExactlySizeValidatorForString.class,
|
ExactlySizeValidatorForString.class,
|
||||||
|
|
|
@ -29,6 +29,7 @@ import org.signal.zkgroup.receipts.ServerZkReceiptOperations;
|
||||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
|
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
|
||||||
import org.whispersystems.textsecuregcm.badges.BadgeTranslator;
|
import org.whispersystems.textsecuregcm.badges.BadgeTranslator;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.BoostConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.SubscriptionLevelConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.SubscriptionLevelConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.configuration.SubscriptionPriceConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.SubscriptionPriceConfiguration;
|
||||||
|
@ -45,14 +46,15 @@ class SubscriptionControllerTest {
|
||||||
|
|
||||||
private static final Clock CLOCK = mock(Clock.class);
|
private static final Clock CLOCK = mock(Clock.class);
|
||||||
private static final SubscriptionConfiguration SUBSCRIPTION_CONFIG = mock(SubscriptionConfiguration.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 SubscriptionManager SUBSCRIPTION_MANAGER = mock(SubscriptionManager.class);
|
||||||
private static final StripeManager STRIPE_MANAGER = mock(StripeManager.class);
|
private static final StripeManager STRIPE_MANAGER = mock(StripeManager.class);
|
||||||
private static final ServerZkReceiptOperations ZK_OPS = mock(ServerZkReceiptOperations.class);
|
private static final ServerZkReceiptOperations ZK_OPS = mock(ServerZkReceiptOperations.class);
|
||||||
private static final IssuedReceiptsManager ISSUED_RECEIPTS_MANAGER = mock(IssuedReceiptsManager.class);
|
private static final IssuedReceiptsManager ISSUED_RECEIPTS_MANAGER = mock(IssuedReceiptsManager.class);
|
||||||
private static final BadgeTranslator BADGE_TRANSLATOR = mock(BadgeTranslator.class);
|
private static final BadgeTranslator BADGE_TRANSLATOR = mock(BadgeTranslator.class);
|
||||||
private static final SubscriptionController SUBSCRIPTION_CONTROLLER = new SubscriptionController(
|
private static final SubscriptionController SUBSCRIPTION_CONTROLLER = new SubscriptionController(
|
||||||
CLOCK, SUBSCRIPTION_CONFIG, SUBSCRIPTION_MANAGER, STRIPE_MANAGER, ZK_OPS, ISSUED_RECEIPTS_MANAGER,
|
CLOCK, SUBSCRIPTION_CONFIG, BOOST_CONFIG, SUBSCRIPTION_MANAGER, STRIPE_MANAGER, ZK_OPS,
|
||||||
BADGE_TRANSLATOR);
|
ISSUED_RECEIPTS_MANAGER, BADGE_TRANSLATOR);
|
||||||
private static final ResourceExtension RESOURCE_EXTENSION = ResourceExtension.builder()
|
private static final ResourceExtension RESOURCE_EXTENSION = ResourceExtension.builder()
|
||||||
.addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE)
|
.addProperty(ServerProperties.UNWRAP_COMPLETION_STAGE_IN_WRITER_ENABLE, Boolean.TRUE)
|
||||||
.addProvider(AuthHelper.getAuthFilter())
|
.addProvider(AuthHelper.getAuthFilter())
|
||||||
|
|
Loading…
Reference in New Issue