Add consolidated subscription configuration API
This commit is contained in:
		
							parent
							
								
									e883d727fb
								
							
						
					
					
						commit
						397d3cb45a
					
				| 
						 | 
					@ -328,27 +328,27 @@ subscription: # configuration for Stripe subscriptions
 | 
				
			||||||
          amount: '10'
 | 
					          amount: '10'
 | 
				
			||||||
          id: price_example # stripe ID
 | 
					          id: price_example # stripe ID
 | 
				
			||||||
 | 
					
 | 
				
			||||||
boost:
 | 
					oneTimeDonations:
 | 
				
			||||||
  level: 1
 | 
					  boost:
 | 
				
			||||||
  expiration: P90D
 | 
					    level: 1
 | 
				
			||||||
  badge: EXAMPLE
 | 
					    expiration: P90D
 | 
				
			||||||
 | 
					    badge: EXAMPLE
 | 
				
			||||||
 | 
					  gift:
 | 
				
			||||||
 | 
					    level: 10
 | 
				
			||||||
 | 
					    expiration: P90D
 | 
				
			||||||
 | 
					    badge: EXAMPLE
 | 
				
			||||||
  currencies:
 | 
					  currencies:
 | 
				
			||||||
    # ISO 4217 currency codes and amounts in those currencies
 | 
					    # ISO 4217 currency codes and amounts in those currencies
 | 
				
			||||||
    xts:
 | 
					    xts:
 | 
				
			||||||
      - '1'
 | 
					      minimum: '0.5'
 | 
				
			||||||
      - '2'
 | 
					      gift: '2'
 | 
				
			||||||
      - '4'
 | 
					      boosts:
 | 
				
			||||||
      - '8'
 | 
					        - '1'
 | 
				
			||||||
      - '20'
 | 
					        - '2'
 | 
				
			||||||
      - '40'
 | 
					        - '4'
 | 
				
			||||||
 | 
					        - '8'
 | 
				
			||||||
gift:
 | 
					        - '20'
 | 
				
			||||||
  level: 10
 | 
					        - '40'
 | 
				
			||||||
  expiration: P90D
 | 
					 | 
				
			||||||
  badge: EXAMPLE
 | 
					 | 
				
			||||||
  currencies:
 | 
					 | 
				
			||||||
    # ISO 4217 currency codes and amounts in those currencies
 | 
					 | 
				
			||||||
    xts: '2'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
registrationService:
 | 
					registrationService:
 | 
				
			||||||
  host: registration.example.com
 | 
					  host: registration.example.com
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,7 +19,6 @@ 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.DatadogConfiguration;
 | 
					import org.whispersystems.textsecuregcm.configuration.DatadogConfiguration;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.configuration.DirectoryConfiguration;
 | 
					import org.whispersystems.textsecuregcm.configuration.DirectoryConfiguration;
 | 
				
			||||||
| 
						 | 
					@ -28,9 +27,9 @@ import org.whispersystems.textsecuregcm.configuration.DynamoDbClientConfiguratio
 | 
				
			||||||
import org.whispersystems.textsecuregcm.configuration.DynamoDbTables;
 | 
					import org.whispersystems.textsecuregcm.configuration.DynamoDbTables;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.configuration.FcmConfiguration;
 | 
					import org.whispersystems.textsecuregcm.configuration.FcmConfiguration;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.configuration.GcpAttachmentsConfiguration;
 | 
					import org.whispersystems.textsecuregcm.configuration.GcpAttachmentsConfiguration;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.configuration.GiftConfiguration;
 | 
					 | 
				
			||||||
import org.whispersystems.textsecuregcm.configuration.MaxDeviceConfiguration;
 | 
					import org.whispersystems.textsecuregcm.configuration.MaxDeviceConfiguration;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.configuration.MessageCacheConfiguration;
 | 
					import org.whispersystems.textsecuregcm.configuration.MessageCacheConfiguration;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.configuration.OneTimeDonationConfiguration;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.configuration.PaymentsServiceConfiguration;
 | 
					import org.whispersystems.textsecuregcm.configuration.PaymentsServiceConfiguration;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
 | 
					import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.configuration.RecaptchaConfiguration;
 | 
					import org.whispersystems.textsecuregcm.configuration.RecaptchaConfiguration;
 | 
				
			||||||
| 
						 | 
					@ -231,12 +230,7 @@ public class WhisperServerConfiguration extends Configuration {
 | 
				
			||||||
  @Valid
 | 
					  @Valid
 | 
				
			||||||
  @JsonProperty
 | 
					  @JsonProperty
 | 
				
			||||||
  @NotNull
 | 
					  @NotNull
 | 
				
			||||||
  private BoostConfiguration boost;
 | 
					  private OneTimeDonationConfiguration oneTimeDonations;
 | 
				
			||||||
 | 
					 | 
				
			||||||
  @Valid
 | 
					 | 
				
			||||||
  @JsonProperty
 | 
					 | 
				
			||||||
  @NotNull
 | 
					 | 
				
			||||||
  private GiftConfiguration gift;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Valid
 | 
					  @Valid
 | 
				
			||||||
  @NotNull
 | 
					  @NotNull
 | 
				
			||||||
| 
						 | 
					@ -411,12 +405,8 @@ public class WhisperServerConfiguration extends Configuration {
 | 
				
			||||||
    return subscription;
 | 
					    return subscription;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public BoostConfiguration getBoost() {
 | 
					  public OneTimeDonationConfiguration getOneTimeDonations() {
 | 
				
			||||||
    return boost;
 | 
					    return oneTimeDonations;
 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  public GiftConfiguration getGift() {
 | 
					 | 
				
			||||||
    return gift;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public ReportMessageConfiguration getReportMessageConfiguration() {
 | 
					  public ReportMessageConfiguration getReportMessageConfiguration() {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -672,19 +672,23 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
 | 
				
			||||||
            ReceiptCredentialPresentation::new),
 | 
					            ReceiptCredentialPresentation::new),
 | 
				
			||||||
        new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, deletedAccountsManager, messagesManager, pushNotificationManager, reportMessageManager, multiRecipientMessageExecutor),
 | 
					        new MessageController(rateLimiters, messageSender, receiptSender, accountsManager, deletedAccountsManager, messagesManager, pushNotificationManager, reportMessageManager, multiRecipientMessageExecutor),
 | 
				
			||||||
        new PaymentsController(currencyManager, paymentsCredentialsGenerator),
 | 
					        new PaymentsController(currencyManager, paymentsCredentialsGenerator),
 | 
				
			||||||
        new ProfileController(clock, rateLimiters, accountsManager, profilesManager, dynamicConfigurationManager, profileBadgeConverter, config.getBadges(), cdnS3Client, profileCdnPolicyGenerator, profileCdnPolicySigner, config.getCdnConfiguration().getBucket(), zkProfileOperations, batchIdentityCheckExecutor),
 | 
					        new ProfileController(clock, rateLimiters, accountsManager, profilesManager, dynamicConfigurationManager,
 | 
				
			||||||
 | 
					            profileBadgeConverter, config.getBadges(), cdnS3Client, profileCdnPolicyGenerator, profileCdnPolicySigner,
 | 
				
			||||||
 | 
					            config.getCdnConfiguration().getBucket(), zkProfileOperations, batchIdentityCheckExecutor),
 | 
				
			||||||
        new ProvisioningController(rateLimiters, provisioningManager),
 | 
					        new ProvisioningController(rateLimiters, provisioningManager),
 | 
				
			||||||
        new RemoteConfigController(remoteConfigsManager, adminEventLogger, config.getRemoteConfigConfiguration().getAuthorizedTokens(), config.getRemoteConfigConfiguration().getGlobalConfig()),
 | 
					        new RemoteConfigController(remoteConfigsManager, adminEventLogger,
 | 
				
			||||||
 | 
					            config.getRemoteConfigConfiguration().getAuthorizedTokens(),
 | 
				
			||||||
 | 
					            config.getRemoteConfigConfiguration().getGlobalConfig()),
 | 
				
			||||||
        new SecureBackupController(backupCredentialsGenerator),
 | 
					        new SecureBackupController(backupCredentialsGenerator),
 | 
				
			||||||
        new SecureStorageController(storageCredentialsGenerator),
 | 
					        new SecureStorageController(storageCredentialsGenerator),
 | 
				
			||||||
        new StickerController(rateLimiters, config.getCdnConfiguration().getAccessKey(),
 | 
					        new StickerController(rateLimiters, config.getCdnConfiguration().getAccessKey(),
 | 
				
			||||||
            config.getCdnConfiguration().getAccessSecret(), config.getCdnConfiguration().getRegion(),
 | 
					            config.getCdnConfiguration().getAccessSecret(), config.getCdnConfiguration().getRegion(),
 | 
				
			||||||
            config.getCdnConfiguration().getBucket())
 | 
					            config.getCdnConfiguration().getBucket())
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    if (config.getSubscription() != null && config.getBoost() != null) {
 | 
					    if (config.getSubscription() != null && config.getOneTimeDonations() != null) {
 | 
				
			||||||
      commonControllers.add(new SubscriptionController(clock, config.getSubscription(), config.getBoost(),
 | 
					      commonControllers.add(new SubscriptionController(clock, config.getSubscription(), config.getOneTimeDonations(),
 | 
				
			||||||
          config.getGift(), subscriptionManager, stripeManager, zkReceiptOperations, issuedReceiptsManager,
 | 
					          subscriptionManager, stripeManager, zkReceiptOperations, issuedReceiptsManager, profileBadgeConverter,
 | 
				
			||||||
          profileBadgeConverter, resourceBundleLevelTranslator));
 | 
					          resourceBundleLevelTranslator));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (Object controller : commonControllers) {
 | 
					    for (Object controller : commonControllers) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,58 +0,0 @@
 | 
				
			||||||
/*
 | 
					 | 
				
			||||||
 * 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.time.Duration;
 | 
					 | 
				
			||||||
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 long level;
 | 
					 | 
				
			||||||
  private final Duration expiration;
 | 
					 | 
				
			||||||
  private final Map<String, List<BigDecimal>> currencies;
 | 
					 | 
				
			||||||
  private final String badge;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @JsonCreator
 | 
					 | 
				
			||||||
  public BoostConfiguration(
 | 
					 | 
				
			||||||
      @JsonProperty("level") long level,
 | 
					 | 
				
			||||||
      @JsonProperty("expiration") Duration expiration,
 | 
					 | 
				
			||||||
      @JsonProperty("currencies") Map<String, List<BigDecimal>> currencies,
 | 
					 | 
				
			||||||
      @JsonProperty("badge") String badge) {
 | 
					 | 
				
			||||||
    this.level = level;
 | 
					 | 
				
			||||||
    this.expiration = expiration;
 | 
					 | 
				
			||||||
    this.currencies = currencies;
 | 
					 | 
				
			||||||
    this.badge = badge;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  public long getLevel() {
 | 
					 | 
				
			||||||
    return level;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @NotNull
 | 
					 | 
				
			||||||
  public Duration getExpiration() {
 | 
					 | 
				
			||||||
    return expiration;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @Valid
 | 
					 | 
				
			||||||
  @NotNull
 | 
					 | 
				
			||||||
  public Map<@NotEmpty String, @Valid @ExactlySize(6) List<@DecimalMin("0.01") @NotNull BigDecimal>> getCurrencies() {
 | 
					 | 
				
			||||||
    return currencies;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @NotEmpty
 | 
					 | 
				
			||||||
  public String getBadge() {
 | 
					 | 
				
			||||||
    return badge;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,21 +0,0 @@
 | 
				
			||||||
/*
 | 
					 | 
				
			||||||
 * Copyright 2022 Signal Messenger, LLC
 | 
					 | 
				
			||||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
package org.whispersystems.textsecuregcm.configuration;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import java.math.BigDecimal;
 | 
					 | 
				
			||||||
import java.time.Duration;
 | 
					 | 
				
			||||||
import java.util.Map;
 | 
					 | 
				
			||||||
import javax.validation.Valid;
 | 
					 | 
				
			||||||
import javax.validation.constraints.DecimalMin;
 | 
					 | 
				
			||||||
import javax.validation.constraints.NotEmpty;
 | 
					 | 
				
			||||||
import javax.validation.constraints.NotNull;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
public record GiftConfiguration(
 | 
					 | 
				
			||||||
    long level,
 | 
					 | 
				
			||||||
    @NotNull Duration expiration,
 | 
					 | 
				
			||||||
    @Valid @NotNull Map<@NotEmpty String, @DecimalMin("0.01") @NotNull BigDecimal> currencies,
 | 
					 | 
				
			||||||
    @NotEmpty String badge) {
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,31 @@
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright 2022 Signal Messenger, LLC
 | 
				
			||||||
 | 
					 * SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package org.whispersystems.textsecuregcm.configuration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.time.Duration;
 | 
				
			||||||
 | 
					import java.util.Map;
 | 
				
			||||||
 | 
					import javax.validation.Valid;
 | 
				
			||||||
 | 
					import javax.validation.constraints.NotEmpty;
 | 
				
			||||||
 | 
					import javax.validation.constraints.Positive;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * @param boost      configuration for individual donations
 | 
				
			||||||
 | 
					 * @param gift       configuration for gift donations
 | 
				
			||||||
 | 
					 * @param currencies map of lower-cased ISO 3 currency codes and the suggested donation amounts in that currency
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public record OneTimeDonationConfiguration(@Valid ExpiringLevelConfiguration boost,
 | 
				
			||||||
 | 
					                                           @Valid ExpiringLevelConfiguration gift,
 | 
				
			||||||
 | 
					                                           Map<String, @Valid OneTimeDonationCurrencyConfiguration> currencies) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * @param badge      the numeric donation level ID
 | 
				
			||||||
 | 
					   * @param level      the badge ID associated with the level
 | 
				
			||||||
 | 
					   * @param expiration the duration after which the level expires
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public record ExpiringLevelConfiguration(@NotEmpty String badge, @Positive long level, Duration expiration) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,30 @@
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright 2022 Signal Messenger, LLC
 | 
				
			||||||
 | 
					 * SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package org.whispersystems.textsecuregcm.configuration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.math.BigDecimal;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					import javax.validation.Valid;
 | 
				
			||||||
 | 
					import javax.validation.constraints.DecimalMin;
 | 
				
			||||||
 | 
					import javax.validation.constraints.NotNull;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.util.ExactlySize;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * One-time donation configuration for a given currency
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param minimum the minimum amount permitted to be charged in this currency
 | 
				
			||||||
 | 
					 * @param gift    the suggested gift donation amount
 | 
				
			||||||
 | 
					 * @param boosts  the list of suggested one-time donation amounts
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public record OneTimeDonationCurrencyConfiguration(
 | 
				
			||||||
 | 
					    @DecimalMin("0.01") BigDecimal minimum,
 | 
				
			||||||
 | 
					    @DecimalMin("0.01") BigDecimal gift,
 | 
				
			||||||
 | 
					    @Valid
 | 
				
			||||||
 | 
					    @ExactlySize(6)
 | 
				
			||||||
 | 
					    @NotNull
 | 
				
			||||||
 | 
					    List<@DecimalMin("0.01") BigDecimal> boosts) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
/*
 | 
					/*
 | 
				
			||||||
 * Copyright 2021 Signal Messenger, LLC
 | 
					 * Copyright 2021-2022 Signal Messenger, LLC
 | 
				
			||||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
					 * SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,16 +15,13 @@ import javax.validation.constraints.NotNull;
 | 
				
			||||||
public class SubscriptionLevelConfiguration {
 | 
					public class SubscriptionLevelConfiguration {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private final String badge;
 | 
					  private final String badge;
 | 
				
			||||||
  private final String product;
 | 
					 | 
				
			||||||
  private final Map<String, SubscriptionPriceConfiguration> prices;
 | 
					  private final Map<String, SubscriptionPriceConfiguration> prices;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @JsonCreator
 | 
					  @JsonCreator
 | 
				
			||||||
  public SubscriptionLevelConfiguration(
 | 
					  public SubscriptionLevelConfiguration(
 | 
				
			||||||
      @JsonProperty("badge") @NotEmpty String badge,
 | 
					      @JsonProperty("badge") @NotEmpty String badge,
 | 
				
			||||||
      @JsonProperty("product") @NotEmpty String product,
 | 
					 | 
				
			||||||
      @JsonProperty("prices") @Valid Map<@NotEmpty String, @NotNull @Valid SubscriptionPriceConfiguration> prices) {
 | 
					      @JsonProperty("prices") @Valid Map<@NotEmpty String, @NotNull @Valid SubscriptionPriceConfiguration> prices) {
 | 
				
			||||||
    this.badge = badge;
 | 
					    this.badge = badge;
 | 
				
			||||||
    this.product = product;
 | 
					 | 
				
			||||||
    this.prices = prices;
 | 
					    this.prices = prices;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -32,10 +29,6 @@ public class SubscriptionLevelConfiguration {
 | 
				
			||||||
    return badge;
 | 
					    return badge;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public String getProduct() {
 | 
					 | 
				
			||||||
    return product;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  public Map<String, SubscriptionPriceConfiguration> getPrices() {
 | 
					  public Map<String, SubscriptionPriceConfiguration> getPrices() {
 | 
				
			||||||
    return prices;
 | 
					    return prices;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,6 +12,7 @@ import com.fasterxml.jackson.annotation.JsonCreator;
 | 
				
			||||||
import com.fasterxml.jackson.annotation.JsonInclude;
 | 
					import com.fasterxml.jackson.annotation.JsonInclude;
 | 
				
			||||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
 | 
					import com.fasterxml.jackson.annotation.JsonInclude.Include;
 | 
				
			||||||
import com.fasterxml.jackson.annotation.JsonProperty;
 | 
					import com.fasterxml.jackson.annotation.JsonProperty;
 | 
				
			||||||
 | 
					import com.google.common.annotations.VisibleForTesting;
 | 
				
			||||||
import com.stripe.exception.StripeException;
 | 
					import com.stripe.exception.StripeException;
 | 
				
			||||||
import com.stripe.model.Charge;
 | 
					import com.stripe.model.Charge;
 | 
				
			||||||
import com.stripe.model.Charge.Outcome;
 | 
					import com.stripe.model.Charge.Outcome;
 | 
				
			||||||
| 
						 | 
					@ -28,8 +29,10 @@ import java.time.Clock;
 | 
				
			||||||
import java.time.Duration;
 | 
					import java.time.Duration;
 | 
				
			||||||
import java.time.Instant;
 | 
					import java.time.Instant;
 | 
				
			||||||
import java.time.temporal.ChronoUnit;
 | 
					import java.time.temporal.ChronoUnit;
 | 
				
			||||||
 | 
					import java.util.Arrays;
 | 
				
			||||||
import java.util.Base64;
 | 
					import java.util.Base64;
 | 
				
			||||||
import java.util.Collection;
 | 
					import java.util.Collection;
 | 
				
			||||||
 | 
					import java.util.HashMap;
 | 
				
			||||||
import java.util.List;
 | 
					import java.util.List;
 | 
				
			||||||
import java.util.Locale;
 | 
					import java.util.Locale;
 | 
				
			||||||
import java.util.Map;
 | 
					import java.util.Map;
 | 
				
			||||||
| 
						 | 
					@ -80,8 +83,8 @@ 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.badges.LevelTranslator;
 | 
					import org.whispersystems.textsecuregcm.badges.LevelTranslator;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.configuration.BoostConfiguration;
 | 
					import org.whispersystems.textsecuregcm.configuration.OneTimeDonationConfiguration;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.configuration.GiftConfiguration;
 | 
					import org.whispersystems.textsecuregcm.configuration.OneTimeDonationCurrencyConfiguration;
 | 
				
			||||||
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;
 | 
				
			||||||
| 
						 | 
					@ -105,22 +108,21 @@ public class SubscriptionController {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private final Clock clock;
 | 
					  private final Clock clock;
 | 
				
			||||||
  private final SubscriptionConfiguration subscriptionConfiguration;
 | 
					  private final SubscriptionConfiguration subscriptionConfiguration;
 | 
				
			||||||
  private final BoostConfiguration boostConfiguration;
 | 
					  private final OneTimeDonationConfiguration oneTimeDonationConfiguration;
 | 
				
			||||||
  private final GiftConfiguration giftConfiguration;
 | 
					 | 
				
			||||||
  private final SubscriptionManager subscriptionManager;
 | 
					  private final SubscriptionManager subscriptionManager;
 | 
				
			||||||
  private final StripeManager stripeManager;
 | 
					  private final StripeManager stripeManager;
 | 
				
			||||||
  private final ServerZkReceiptOperations zkReceiptOperations;
 | 
					  private final ServerZkReceiptOperations zkReceiptOperations;
 | 
				
			||||||
  private final IssuedReceiptsManager issuedReceiptsManager;
 | 
					  private final IssuedReceiptsManager issuedReceiptsManager;
 | 
				
			||||||
  private final BadgeTranslator badgeTranslator;
 | 
					  private final BadgeTranslator badgeTranslator;
 | 
				
			||||||
  private final LevelTranslator levelTranslator;
 | 
					  private final LevelTranslator levelTranslator;
 | 
				
			||||||
 | 
					  private final Map<String, CurrencyConfiguration> currencyConfiguration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private static final String INVALID_ACCEPT_LANGUAGE_COUNTER_NAME = name(SubscriptionController.class, "invalidAcceptLanguage");
 | 
					  private static final String INVALID_ACCEPT_LANGUAGE_COUNTER_NAME = name(SubscriptionController.class, "invalidAcceptLanguage");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public SubscriptionController(
 | 
					  public SubscriptionController(
 | 
				
			||||||
      @Nonnull Clock clock,
 | 
					      @Nonnull Clock clock,
 | 
				
			||||||
      @Nonnull SubscriptionConfiguration subscriptionConfiguration,
 | 
					      @Nonnull SubscriptionConfiguration subscriptionConfiguration,
 | 
				
			||||||
      @Nonnull BoostConfiguration boostConfiguration,
 | 
					      @Nonnull OneTimeDonationConfiguration oneTimeDonationConfiguration,
 | 
				
			||||||
      @Nonnull GiftConfiguration giftConfiguration,
 | 
					 | 
				
			||||||
      @Nonnull SubscriptionManager subscriptionManager,
 | 
					      @Nonnull SubscriptionManager subscriptionManager,
 | 
				
			||||||
      @Nonnull StripeManager stripeManager,
 | 
					      @Nonnull StripeManager stripeManager,
 | 
				
			||||||
      @Nonnull ServerZkReceiptOperations zkReceiptOperations,
 | 
					      @Nonnull ServerZkReceiptOperations zkReceiptOperations,
 | 
				
			||||||
| 
						 | 
					@ -129,14 +131,84 @@ public class SubscriptionController {
 | 
				
			||||||
      @Nonnull LevelTranslator levelTranslator) {
 | 
					      @Nonnull LevelTranslator levelTranslator) {
 | 
				
			||||||
    this.clock = Objects.requireNonNull(clock);
 | 
					    this.clock = Objects.requireNonNull(clock);
 | 
				
			||||||
    this.subscriptionConfiguration = Objects.requireNonNull(subscriptionConfiguration);
 | 
					    this.subscriptionConfiguration = Objects.requireNonNull(subscriptionConfiguration);
 | 
				
			||||||
    this.boostConfiguration = Objects.requireNonNull(boostConfiguration);
 | 
					    this.oneTimeDonationConfiguration = Objects.requireNonNull(oneTimeDonationConfiguration);
 | 
				
			||||||
    this.giftConfiguration = Objects.requireNonNull(giftConfiguration);
 | 
					 | 
				
			||||||
    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);
 | 
				
			||||||
    this.issuedReceiptsManager = Objects.requireNonNull(issuedReceiptsManager);
 | 
					    this.issuedReceiptsManager = Objects.requireNonNull(issuedReceiptsManager);
 | 
				
			||||||
    this.badgeTranslator = Objects.requireNonNull(badgeTranslator);
 | 
					    this.badgeTranslator = Objects.requireNonNull(badgeTranslator);
 | 
				
			||||||
    this.levelTranslator = Objects.requireNonNull(levelTranslator);
 | 
					    this.levelTranslator = Objects.requireNonNull(levelTranslator);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.currencyConfiguration = buildCurrencyConfiguration(this.oneTimeDonationConfiguration,
 | 
				
			||||||
 | 
					        this.subscriptionConfiguration, List.of(stripeManager));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private static Map<String, CurrencyConfiguration> buildCurrencyConfiguration(
 | 
				
			||||||
 | 
					      OneTimeDonationConfiguration oneTimeDonationConfiguration,
 | 
				
			||||||
 | 
					      SubscriptionConfiguration subscriptionConfiguration,
 | 
				
			||||||
 | 
					      List<SubscriptionProcessorManager> subscriptionProcessorManagers) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return oneTimeDonationConfiguration.currencies()
 | 
				
			||||||
 | 
					        .entrySet().stream()
 | 
				
			||||||
 | 
					        .collect(Collectors.toMap(Entry::getKey, currencyAndConfig -> {
 | 
				
			||||||
 | 
					          final String currency = currencyAndConfig.getKey();
 | 
				
			||||||
 | 
					          final OneTimeDonationCurrencyConfiguration currencyConfig = currencyAndConfig.getValue();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          final Map<String, List<BigDecimal>> oneTimeLevelsToSuggestedAmounts = Map.of(
 | 
				
			||||||
 | 
					              String.valueOf(oneTimeDonationConfiguration.boost().level()), currencyConfig.boosts(),
 | 
				
			||||||
 | 
					              String.valueOf(oneTimeDonationConfiguration.gift().level()), List.of(currencyConfig.gift())
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          final Map<String, BigDecimal> subscriptionLevelsToAmounts = subscriptionConfiguration.getLevels()
 | 
				
			||||||
 | 
					              .entrySet().stream()
 | 
				
			||||||
 | 
					              .filter(levelIdAndConfig -> levelIdAndConfig.getValue().getPrices().containsKey(currency))
 | 
				
			||||||
 | 
					              .collect(Collectors.toMap(
 | 
				
			||||||
 | 
					                  levelIdAndConfig -> String.valueOf(levelIdAndConfig.getKey()),
 | 
				
			||||||
 | 
					                  levelIdAndConfig -> levelIdAndConfig.getValue().getPrices().get(currency).getAmount()));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          final List<String> supportedPaymentMethods = Arrays.stream(PaymentMethod.values())
 | 
				
			||||||
 | 
					              .filter(paymentMethod -> subscriptionProcessorManagers.stream()
 | 
				
			||||||
 | 
					                  .anyMatch(manager -> manager.getSupportedCurrencies().contains(currency)
 | 
				
			||||||
 | 
					                      && manager.supportsPaymentMethod(paymentMethod)))
 | 
				
			||||||
 | 
					              .map(PaymentMethod::name)
 | 
				
			||||||
 | 
					              .collect(Collectors.toList());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (supportedPaymentMethods.isEmpty()) {
 | 
				
			||||||
 | 
					            throw new RuntimeException("Configuration has currency with no processor support: " + currency);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          return new CurrencyConfiguration(currencyConfig.minimum(), oneTimeLevelsToSuggestedAmounts,
 | 
				
			||||||
 | 
					              subscriptionLevelsToAmounts, supportedPaymentMethods);
 | 
				
			||||||
 | 
					        }));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @VisibleForTesting
 | 
				
			||||||
 | 
					  GetSubscriptionConfigurationResponse buildGetSubscriptionConfigurationResponse(List<Locale> acceptableLanguages) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final Map<String, LevelConfiguration> levels = new HashMap<>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    subscriptionConfiguration.getLevels().forEach((levelId, levelConfig) -> {
 | 
				
			||||||
 | 
					      final LevelConfiguration levelConfiguration = new LevelConfiguration(
 | 
				
			||||||
 | 
					          levelTranslator.translate(acceptableLanguages, levelConfig.getBadge()),
 | 
				
			||||||
 | 
					          badgeTranslator.translate(acceptableLanguages, levelConfig.getBadge()));
 | 
				
			||||||
 | 
					      levels.put(String.valueOf(levelId), levelConfiguration);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    levels.put(String.valueOf(oneTimeDonationConfiguration.boost().level()),
 | 
				
			||||||
 | 
					        new LevelConfiguration(
 | 
				
			||||||
 | 
					            levelTranslator.translate(acceptableLanguages, oneTimeDonationConfiguration.boost().badge()),
 | 
				
			||||||
 | 
					            // NB: the one-time badges are PurchasableBadge, which has a `duration` field
 | 
				
			||||||
 | 
					            new PurchasableBadge(
 | 
				
			||||||
 | 
					                badgeTranslator.translate(acceptableLanguages, oneTimeDonationConfiguration.boost().badge()),
 | 
				
			||||||
 | 
					                oneTimeDonationConfiguration.boost().expiration())));
 | 
				
			||||||
 | 
					    levels.put(String.valueOf(oneTimeDonationConfiguration.gift().level()),
 | 
				
			||||||
 | 
					        new LevelConfiguration(
 | 
				
			||||||
 | 
					            levelTranslator.translate(acceptableLanguages, oneTimeDonationConfiguration.gift().badge()),
 | 
				
			||||||
 | 
					            new PurchasableBadge(
 | 
				
			||||||
 | 
					                badgeTranslator.translate(acceptableLanguages, oneTimeDonationConfiguration.gift().badge()),
 | 
				
			||||||
 | 
					                oneTimeDonationConfiguration.gift().expiration())));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return new GetSubscriptionConfigurationResponse(currencyConfiguration, levels);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Timed
 | 
					  @Timed
 | 
				
			||||||
| 
						 | 
					@ -463,10 +535,58 @@ public class SubscriptionController {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Comprehensive configuration for subscriptions and one-time donations
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param currencies map of lower-cased ISO 3 currency codes to minimums and level-specific scalar amounts
 | 
				
			||||||
 | 
					   * @param levels     map of numeric level IDs to level-specific configuration
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public record GetSubscriptionConfigurationResponse(Map<String, CurrencyConfiguration> currencies,
 | 
				
			||||||
 | 
					                                                     Map<String, LevelConfiguration> levels) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Configuration for a currency - use to present appropriate client interfaces
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param minimum                 the minimum amount that may be submitted for a one-time donation in the currency
 | 
				
			||||||
 | 
					   * @param oneTime                 map of numeric one-time donation level IDs to the list of default amounts to be
 | 
				
			||||||
 | 
					   *                                presented
 | 
				
			||||||
 | 
					   * @param subscription            map of numeric subscription level IDs to the amount charged for that level
 | 
				
			||||||
 | 
					   * @param supportedPaymentMethods the payment methods that support the given currency
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public record CurrencyConfiguration(BigDecimal minimum, Map<String, List<BigDecimal>> oneTime,
 | 
				
			||||||
 | 
					                                      Map<String, BigDecimal> subscription,
 | 
				
			||||||
 | 
					                                      List<String> supportedPaymentMethods) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Configuration for a donation level - use to present appropriate client interfaces
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @param name  the localized name for the level
 | 
				
			||||||
 | 
					   * @param badge the displayable badge associated with the level
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public record LevelConfiguration(String name, Badge badge) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Timed
 | 
				
			||||||
 | 
					  @GET
 | 
				
			||||||
 | 
					  @Path("/configuration")
 | 
				
			||||||
 | 
					  @Produces(MediaType.APPLICATION_JSON)
 | 
				
			||||||
 | 
					  public CompletableFuture<Response> getConfiguration(@Context ContainerRequestContext containerRequestContext) {
 | 
				
			||||||
 | 
					    return CompletableFuture.supplyAsync(() -> {
 | 
				
			||||||
 | 
					      List<Locale> acceptableLanguages = getAcceptableLanguagesForRequest(containerRequestContext);
 | 
				
			||||||
 | 
					      return Response.ok(buildGetSubscriptionConfigurationResponse(acceptableLanguages)).build();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Timed
 | 
					  @Timed
 | 
				
			||||||
  @GET
 | 
					  @GET
 | 
				
			||||||
  @Path("/levels")
 | 
					  @Path("/levels")
 | 
				
			||||||
  @Produces(MediaType.APPLICATION_JSON)
 | 
					  @Produces(MediaType.APPLICATION_JSON)
 | 
				
			||||||
 | 
					  @Deprecated // use /configuration
 | 
				
			||||||
  public CompletableFuture<Response> getLevels(@Context ContainerRequestContext containerRequestContext) {
 | 
					  public CompletableFuture<Response> getLevels(@Context ContainerRequestContext containerRequestContext) {
 | 
				
			||||||
    return CompletableFuture.supplyAsync(() -> {
 | 
					    return CompletableFuture.supplyAsync(() -> {
 | 
				
			||||||
      List<Locale> acceptableLanguages = getAcceptableLanguagesForRequest(containerRequestContext);
 | 
					      List<Locale> acceptableLanguages = getAcceptableLanguagesForRequest(containerRequestContext);
 | 
				
			||||||
| 
						 | 
					@ -514,16 +634,21 @@ public class SubscriptionController {
 | 
				
			||||||
  @GET
 | 
					  @GET
 | 
				
			||||||
  @Path("/boost/badges")
 | 
					  @Path("/boost/badges")
 | 
				
			||||||
  @Produces(MediaType.APPLICATION_JSON)
 | 
					  @Produces(MediaType.APPLICATION_JSON)
 | 
				
			||||||
 | 
					  @Deprecated // use /configuration
 | 
				
			||||||
  public CompletableFuture<Response> getBoostBadges(@Context ContainerRequestContext containerRequestContext) {
 | 
					  public CompletableFuture<Response> getBoostBadges(@Context ContainerRequestContext containerRequestContext) {
 | 
				
			||||||
    return CompletableFuture.supplyAsync(() -> {
 | 
					    return CompletableFuture.supplyAsync(() -> {
 | 
				
			||||||
      long boostLevel = boostConfiguration.getLevel();
 | 
					      long boostLevel = oneTimeDonationConfiguration.boost().level();
 | 
				
			||||||
      String boostBadge = boostConfiguration.getBadge();
 | 
					      String boostBadge = oneTimeDonationConfiguration.boost().badge();
 | 
				
			||||||
      long giftLevel = giftConfiguration.level();
 | 
					      long giftLevel = oneTimeDonationConfiguration.gift().level();
 | 
				
			||||||
      String giftBadge = giftConfiguration.badge();
 | 
					      String giftBadge = oneTimeDonationConfiguration.gift().badge();
 | 
				
			||||||
      List<Locale> acceptableLanguages = getAcceptableLanguagesForRequest(containerRequestContext);
 | 
					      List<Locale> acceptableLanguages = getAcceptableLanguagesForRequest(containerRequestContext);
 | 
				
			||||||
      GetBoostBadgesResponse getBoostBadgesResponse = new GetBoostBadgesResponse(Map.of(
 | 
					      GetBoostBadgesResponse getBoostBadgesResponse = new GetBoostBadgesResponse(Map.of(
 | 
				
			||||||
          boostLevel, new GetBoostBadgesResponse.Level(new PurchasableBadge(badgeTranslator.translate(acceptableLanguages, boostBadge), boostConfiguration.getExpiration())),
 | 
					          boostLevel, new GetBoostBadgesResponse.Level(
 | 
				
			||||||
          giftLevel, new GetBoostBadgesResponse.Level(new PurchasableBadge(badgeTranslator.translate(acceptableLanguages, giftBadge), giftConfiguration.expiration()))));
 | 
					              new PurchasableBadge(badgeTranslator.translate(acceptableLanguages, boostBadge),
 | 
				
			||||||
 | 
					                  oneTimeDonationConfiguration.boost().expiration())),
 | 
				
			||||||
 | 
					          giftLevel, new GetBoostBadgesResponse.Level(
 | 
				
			||||||
 | 
					              new PurchasableBadge(badgeTranslator.translate(acceptableLanguages, giftBadge),
 | 
				
			||||||
 | 
					                  oneTimeDonationConfiguration.gift().expiration()))));
 | 
				
			||||||
      return Response.ok(getBoostBadgesResponse).build();
 | 
					      return Response.ok(getBoostBadgesResponse).build();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -532,20 +657,24 @@ public class SubscriptionController {
 | 
				
			||||||
  @GET
 | 
					  @GET
 | 
				
			||||||
  @Path("/boost/amounts")
 | 
					  @Path("/boost/amounts")
 | 
				
			||||||
  @Produces(MediaType.APPLICATION_JSON)
 | 
					  @Produces(MediaType.APPLICATION_JSON)
 | 
				
			||||||
 | 
					  @Deprecated // use /configuration
 | 
				
			||||||
  public CompletableFuture<Response> getBoostAmounts() {
 | 
					  public CompletableFuture<Response> getBoostAmounts() {
 | 
				
			||||||
    return CompletableFuture.supplyAsync(() -> Response.ok(
 | 
					    return CompletableFuture.supplyAsync(() -> Response.ok(
 | 
				
			||||||
        boostConfiguration.getCurrencies().entrySet().stream().collect(
 | 
					            oneTimeDonationConfiguration.currencies().entrySet().stream().collect(
 | 
				
			||||||
            Collectors.toMap(entry -> entry.getKey().toUpperCase(Locale.ROOT), Entry::getValue))).build());
 | 
					                Collectors.toMap(entry -> entry.getKey().toUpperCase(Locale.ROOT), entry -> entry.getValue().boosts())))
 | 
				
			||||||
 | 
					        .build());
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Timed
 | 
					  @Timed
 | 
				
			||||||
  @GET
 | 
					  @GET
 | 
				
			||||||
  @Path("/boost/amounts/gift")
 | 
					  @Path("/boost/amounts/gift")
 | 
				
			||||||
  @Produces(MediaType.APPLICATION_JSON)
 | 
					  @Produces(MediaType.APPLICATION_JSON)
 | 
				
			||||||
 | 
					  @Deprecated // use /configuration
 | 
				
			||||||
  public CompletableFuture<Response> getGiftAmounts() {
 | 
					  public CompletableFuture<Response> getGiftAmounts() {
 | 
				
			||||||
    return CompletableFuture.supplyAsync(() -> Response.ok(
 | 
					    return CompletableFuture.supplyAsync(() -> Response.ok(
 | 
				
			||||||
        giftConfiguration.currencies().entrySet().stream().collect(
 | 
					            oneTimeDonationConfiguration.currencies().entrySet().stream().collect(
 | 
				
			||||||
            Collectors.toMap(entry -> entry.getKey().toUpperCase(Locale.ROOT), Entry::getValue))).build());
 | 
					                Collectors.toMap(entry -> entry.getKey().toUpperCase(Locale.ROOT), entry -> entry.getValue().gift())))
 | 
				
			||||||
 | 
					        .build());
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public static class CreateBoostRequest {
 | 
					  public static class CreateBoostRequest {
 | 
				
			||||||
| 
						 | 
					@ -576,16 +705,20 @@ public class SubscriptionController {
 | 
				
			||||||
  @Produces(MediaType.APPLICATION_JSON)
 | 
					  @Produces(MediaType.APPLICATION_JSON)
 | 
				
			||||||
  public CompletableFuture<Response> createBoostPaymentIntent(@NotNull @Valid CreateBoostRequest request) {
 | 
					  public CompletableFuture<Response> createBoostPaymentIntent(@NotNull @Valid CreateBoostRequest request) {
 | 
				
			||||||
    return CompletableFuture.runAsync(() -> {
 | 
					    return CompletableFuture.runAsync(() -> {
 | 
				
			||||||
      if (request.level == null) {
 | 
					          if (request.level == null) {
 | 
				
			||||||
        request.level = boostConfiguration.getLevel();
 | 
					            request.level = oneTimeDonationConfiguration.boost().level();
 | 
				
			||||||
      }
 | 
					          }
 | 
				
			||||||
      if (request.level == giftConfiguration.level()) {
 | 
					          if (request.level == oneTimeDonationConfiguration.gift().level()) {
 | 
				
			||||||
        BigDecimal amountConfigured = giftConfiguration.currencies().get(request.currency.toLowerCase(Locale.ROOT));
 | 
					            BigDecimal amountConfigured = oneTimeDonationConfiguration.currencies()
 | 
				
			||||||
        if (amountConfigured == null || stripeManager.convertConfiguredAmountToStripeAmount(request.currency, amountConfigured).compareTo(BigDecimal.valueOf(request.amount)) != 0) {
 | 
					                .get(request.currency.toLowerCase(Locale.ROOT)).gift();
 | 
				
			||||||
          throw new WebApplicationException(Response.status(Status.CONFLICT).entity(Map.of("error", "level_amount_mismatch")).build());
 | 
					            if (amountConfigured == null ||
 | 
				
			||||||
        }
 | 
					                stripeManager.convertConfiguredAmountToStripeAmount(request.currency, amountConfigured)
 | 
				
			||||||
      }
 | 
					                    .compareTo(BigDecimal.valueOf(request.amount)) != 0) {
 | 
				
			||||||
    })
 | 
					              throw new WebApplicationException(
 | 
				
			||||||
 | 
					                  Response.status(Status.CONFLICT).entity(Map.of("error", "level_amount_mismatch")).build());
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
        .thenCompose(unused -> stripeManager.createPaymentIntent(request.currency, request.amount, request.level))
 | 
					        .thenCompose(unused -> stripeManager.createPaymentIntent(request.currency, request.amount, request.level))
 | 
				
			||||||
        .thenApply(paymentIntent -> Response.ok(new CreateBoostResponse(paymentIntent.getClientSecret())).build());
 | 
					        .thenApply(paymentIntent -> Response.ok(new CreateBoostResponse(paymentIntent.getClientSecret())).build());
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -627,21 +760,23 @@ public class SubscriptionController {
 | 
				
			||||||
          if (!StringUtils.equalsIgnoreCase("succeeded", paymentIntent.getStatus())) {
 | 
					          if (!StringUtils.equalsIgnoreCase("succeeded", paymentIntent.getStatus())) {
 | 
				
			||||||
            throw new WebApplicationException(Status.PAYMENT_REQUIRED);
 | 
					            throw new WebApplicationException(Status.PAYMENT_REQUIRED);
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          long level = boostConfiguration.getLevel();
 | 
					          long level = oneTimeDonationConfiguration.boost().level();
 | 
				
			||||||
          if (paymentIntent.getMetadata() != null) {
 | 
					          if (paymentIntent.getMetadata() != null) {
 | 
				
			||||||
            String levelMetadata = paymentIntent.getMetadata().getOrDefault("level", Long.toString(boostConfiguration.getLevel()));
 | 
					            String levelMetadata = paymentIntent.getMetadata()
 | 
				
			||||||
 | 
					                .getOrDefault("level", Long.toString(oneTimeDonationConfiguration.boost().level()));
 | 
				
			||||||
            try {
 | 
					            try {
 | 
				
			||||||
              level = Long.parseLong(levelMetadata);
 | 
					              level = Long.parseLong(levelMetadata);
 | 
				
			||||||
            } catch (NumberFormatException e) {
 | 
					            } catch (NumberFormatException e) {
 | 
				
			||||||
              logger.error("failed to parse level metadata ({}) on payment intent {}", levelMetadata, paymentIntent.getId(), e);
 | 
					              logger.error("failed to parse level metadata ({}) on payment intent {}", levelMetadata,
 | 
				
			||||||
 | 
					                  paymentIntent.getId(), e);
 | 
				
			||||||
              throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
 | 
					              throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          Duration levelExpiration;
 | 
					          Duration levelExpiration;
 | 
				
			||||||
          if (boostConfiguration.getLevel() == level) {
 | 
					          if (oneTimeDonationConfiguration.boost().level() == level) {
 | 
				
			||||||
            levelExpiration = boostConfiguration.getExpiration();
 | 
					            levelExpiration = oneTimeDonationConfiguration.boost().expiration();
 | 
				
			||||||
          } else if (giftConfiguration.level() == level) {
 | 
					          } else if (oneTimeDonationConfiguration.gift().level() == level) {
 | 
				
			||||||
            levelExpiration = giftConfiguration.expiration();
 | 
					            levelExpiration = oneTimeDonationConfiguration.gift().expiration();
 | 
				
			||||||
          } else {
 | 
					          } else {
 | 
				
			||||||
            logger.error("level ({}) returned from payment intent that is unknown to the server", level);
 | 
					            logger.error("level ({}) returned from payment intent that is unknown to the server", level);
 | 
				
			||||||
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
 | 
					            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -47,6 +47,7 @@ import java.util.List;
 | 
				
			||||||
import java.util.Locale;
 | 
					import java.util.Locale;
 | 
				
			||||||
import java.util.Map;
 | 
					import java.util.Map;
 | 
				
			||||||
import java.util.Objects;
 | 
					import java.util.Objects;
 | 
				
			||||||
 | 
					import java.util.Set;
 | 
				
			||||||
import java.util.concurrent.CompletableFuture;
 | 
					import java.util.concurrent.CompletableFuture;
 | 
				
			||||||
import java.util.concurrent.CompletionException;
 | 
					import java.util.concurrent.CompletionException;
 | 
				
			||||||
import java.util.concurrent.Executor;
 | 
					import java.util.concurrent.Executor;
 | 
				
			||||||
| 
						 | 
					@ -65,6 +66,18 @@ public class StripeManager implements SubscriptionProcessorManager {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private static final String METADATA_KEY_LEVEL = "level";
 | 
					  private static final String METADATA_KEY_LEVEL = "level";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // https://stripe.com/docs/currencies?presentment-currency=US
 | 
				
			||||||
 | 
					  private static final Set<String> SUPPORTED_CURRENCIES = Set.of(
 | 
				
			||||||
 | 
					      "aed", "afn", "all", "amd", "ang", "aoa", "ars", "aud", "awg", "azn", "bam", "bbd", "bdt", "bgn", "bif", "bmd",
 | 
				
			||||||
 | 
					      "bnd", "bob", "brl", "bsd", "bwp", "bzd", "cad", "cdf", "chf", "clp", "cny", "cop", "crc", "cve", "czk", "djf",
 | 
				
			||||||
 | 
					      "dkk", "dop", "dzd", "egp", "etb", "eur", "fjd", "fkp", "gbp", "gel", "gip", "gmd", "gnf", "gtq", "gyd", "hkd",
 | 
				
			||||||
 | 
					      "hnl", "hrk", "htg", "huf", "idr", "ils", "inr", "isk", "jmd", "jpy", "kes", "kgs", "khr", "kmf", "krw", "kyd",
 | 
				
			||||||
 | 
					      "kzt", "lak", "lbp", "lkr", "lrd", "lsl", "mad", "mdl", "mga", "mkd", "mmk", "mnt", "mop", "mro", "mur", "mvr",
 | 
				
			||||||
 | 
					      "mwk", "mxn", "myr", "mzn", "nad", "ngn", "nio", "nok", "npr", "nzd", "pab", "pen", "pgk", "php", "pkr", "pln",
 | 
				
			||||||
 | 
					      "pyg", "qar", "ron", "rsd", "rub", "rwf", "sar", "sbd", "scr", "sek", "sgd", "shp", "sll", "sos", "srd", "std",
 | 
				
			||||||
 | 
					      "szl", "thb", "tjs", "top", "try", "ttd", "twd", "tzs", "uah", "ugx", "usd", "uyu", "uzs", "vnd", "vuv", "wst",
 | 
				
			||||||
 | 
					      "xaf", "xcd", "xof", "xpf", "yer", "zar", "zmw");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private final String apiKey;
 | 
					  private final String apiKey;
 | 
				
			||||||
  private final Executor executor;
 | 
					  private final Executor executor;
 | 
				
			||||||
  private final byte[] idempotencyKeyGenerator;
 | 
					  private final byte[] idempotencyKeyGenerator;
 | 
				
			||||||
| 
						 | 
					@ -166,6 +179,11 @@ public class StripeManager implements SubscriptionProcessorManager {
 | 
				
			||||||
        .thenApply(SetupIntent::getClientSecret);
 | 
					        .thenApply(SetupIntent::getClientSecret);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Override
 | 
				
			||||||
 | 
					  public Set<String> getSupportedCurrencies() {
 | 
				
			||||||
 | 
					    return SUPPORTED_CURRENCIES;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Creates a payment intent. May throw a 400 WebApplicationException if the amount is too small.
 | 
					   * Creates a payment intent. May throw a 400 WebApplicationException if the amount is too small.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
package org.whispersystems.textsecuregcm.subscriptions;
 | 
					package org.whispersystems.textsecuregcm.subscriptions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.Set;
 | 
				
			||||||
import java.util.concurrent.CompletableFuture;
 | 
					import java.util.concurrent.CompletableFuture;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public interface SubscriptionProcessorManager {
 | 
					public interface SubscriptionProcessorManager {
 | 
				
			||||||
| 
						 | 
					@ -16,4 +17,6 @@ public interface SubscriptionProcessorManager {
 | 
				
			||||||
  CompletableFuture<ProcessorCustomer> createCustomer(byte[] subscriberUser);
 | 
					  CompletableFuture<ProcessorCustomer> createCustomer(byte[] subscriberUser);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  CompletableFuture<String> createPaymentMethodSetupToken(String customerId);
 | 
					  CompletableFuture<String> createPaymentMethodSetupToken(String customerId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Set<String> getSupportedCurrencies();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,12 +15,15 @@ import static org.mockito.Mockito.when;
 | 
				
			||||||
import static org.whispersystems.textsecuregcm.util.AttributeValues.b;
 | 
					import static org.whispersystems.textsecuregcm.util.AttributeValues.b;
 | 
				
			||||||
import static org.whispersystems.textsecuregcm.util.AttributeValues.n;
 | 
					import static org.whispersystems.textsecuregcm.util.AttributeValues.n;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
 | 
				
			||||||
 | 
					import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
 | 
				
			||||||
import com.stripe.exception.ApiException;
 | 
					import com.stripe.exception.ApiException;
 | 
				
			||||||
import com.stripe.model.Subscription;
 | 
					import com.stripe.model.Subscription;
 | 
				
			||||||
import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
 | 
					import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider;
 | 
				
			||||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
 | 
					import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
 | 
				
			||||||
import io.dropwizard.testing.junit5.ResourceExtension;
 | 
					import io.dropwizard.testing.junit5.ResourceExtension;
 | 
				
			||||||
import java.math.BigDecimal;
 | 
					import java.math.BigDecimal;
 | 
				
			||||||
 | 
					import java.math.RoundingMode;
 | 
				
			||||||
import java.time.Clock;
 | 
					import java.time.Clock;
 | 
				
			||||||
import java.time.Instant;
 | 
					import java.time.Instant;
 | 
				
			||||||
import java.util.Arrays;
 | 
					import java.util.Arrays;
 | 
				
			||||||
| 
						 | 
					@ -32,6 +35,7 @@ import java.util.Set;
 | 
				
			||||||
import java.util.UUID;
 | 
					import java.util.UUID;
 | 
				
			||||||
import java.util.concurrent.CompletableFuture;
 | 
					import java.util.concurrent.CompletableFuture;
 | 
				
			||||||
import java.util.concurrent.CompletionException;
 | 
					import java.util.concurrent.CompletionException;
 | 
				
			||||||
 | 
					import java.util.function.Predicate;
 | 
				
			||||||
import javax.ws.rs.client.Entity;
 | 
					import javax.ws.rs.client.Entity;
 | 
				
			||||||
import javax.ws.rs.core.Response;
 | 
					import javax.ws.rs.core.Response;
 | 
				
			||||||
import org.glassfish.jersey.server.ServerProperties;
 | 
					import org.glassfish.jersey.server.ServerProperties;
 | 
				
			||||||
| 
						 | 
					@ -46,16 +50,15 @@ 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.badges.LevelTranslator;
 | 
					import org.whispersystems.textsecuregcm.badges.LevelTranslator;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.configuration.BoostConfiguration;
 | 
					import org.whispersystems.textsecuregcm.configuration.OneTimeDonationConfiguration;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.configuration.GiftConfiguration;
 | 
					 | 
				
			||||||
import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration;
 | 
					import org.whispersystems.textsecuregcm.configuration.SubscriptionConfiguration;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.configuration.SubscriptionLevelConfiguration;
 | 
					 | 
				
			||||||
import org.whispersystems.textsecuregcm.configuration.SubscriptionPriceConfiguration;
 | 
					 | 
				
			||||||
import org.whispersystems.textsecuregcm.controllers.SubscriptionController.GetLevelsResponse;
 | 
					import org.whispersystems.textsecuregcm.controllers.SubscriptionController.GetLevelsResponse;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.controllers.SubscriptionController.GetSubscriptionConfigurationResponse;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.entities.Badge;
 | 
					import org.whispersystems.textsecuregcm.entities.Badge;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.entities.BadgeSvg;
 | 
					import org.whispersystems.textsecuregcm.entities.BadgeSvg;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager;
 | 
					import org.whispersystems.textsecuregcm.storage.IssuedReceiptsManager;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.SubscriptionManager;
 | 
					import org.whispersystems.textsecuregcm.storage.SubscriptionManager;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.subscriptions.PaymentMethod;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.subscriptions.ProcessorCustomer;
 | 
					import org.whispersystems.textsecuregcm.subscriptions.ProcessorCustomer;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.subscriptions.StripeManager;
 | 
					import org.whispersystems.textsecuregcm.subscriptions.StripeManager;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessor;
 | 
					import org.whispersystems.textsecuregcm.subscriptions.SubscriptionProcessor;
 | 
				
			||||||
| 
						 | 
					@ -67,17 +70,31 @@ import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
 | 
				
			||||||
class SubscriptionControllerTest {
 | 
					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 BoostConfiguration BOOST_CONFIG = mock(BoostConfiguration.class);
 | 
					  private static final YAMLMapper YAML_MAPPER = new YAMLMapper();
 | 
				
			||||||
  private static final GiftConfiguration GIFT_CONFIG = mock(GiftConfiguration.class);
 | 
					
 | 
				
			||||||
 | 
					  static {
 | 
				
			||||||
 | 
					    YAML_MAPPER.registerModule(new JavaTimeModule());
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private static final SubscriptionConfiguration SUBSCRIPTION_CONFIG = ConfigHelper.getSubscriptionConfig();
 | 
				
			||||||
 | 
					  private static final OneTimeDonationConfiguration ONETIME_CONFIG = ConfigHelper.getOneTimeConfig();
 | 
				
			||||||
  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);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static {
 | 
				
			||||||
 | 
					    when(STRIPE_MANAGER.getSupportedCurrencies())
 | 
				
			||||||
 | 
					        .thenCallRealMethod();
 | 
				
			||||||
 | 
					    when(STRIPE_MANAGER.supportsPaymentMethod(PaymentMethod.CARD))
 | 
				
			||||||
 | 
					        .thenCallRealMethod();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  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 LevelTranslator LEVEL_TRANSLATOR = mock(LevelTranslator.class);
 | 
					  private static final LevelTranslator LEVEL_TRANSLATOR = mock(LevelTranslator.class);
 | 
				
			||||||
  private static final SubscriptionController SUBSCRIPTION_CONTROLLER = new SubscriptionController(
 | 
					  private static final SubscriptionController SUBSCRIPTION_CONTROLLER = new SubscriptionController(
 | 
				
			||||||
      CLOCK, SUBSCRIPTION_CONFIG, BOOST_CONFIG, GIFT_CONFIG, SUBSCRIPTION_MANAGER, STRIPE_MANAGER, ZK_OPS,
 | 
					      CLOCK, SUBSCRIPTION_CONFIG, ONETIME_CONFIG, SUBSCRIPTION_MANAGER, STRIPE_MANAGER, ZK_OPS,
 | 
				
			||||||
      ISSUED_RECEIPTS_MANAGER, BADGE_TRANSLATOR, LEVEL_TRANSLATOR);
 | 
					      ISSUED_RECEIPTS_MANAGER, BADGE_TRANSLATOR, LEVEL_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)
 | 
				
			||||||
| 
						 | 
					@ -96,7 +113,7 @@ class SubscriptionControllerTest {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @AfterEach
 | 
					  @AfterEach
 | 
				
			||||||
  void tearDown() {
 | 
					  void tearDown() {
 | 
				
			||||||
    reset(CLOCK, SUBSCRIPTION_CONFIG, SUBSCRIPTION_MANAGER, STRIPE_MANAGER, ZK_OPS, ISSUED_RECEIPTS_MANAGER,
 | 
					    reset(CLOCK, SUBSCRIPTION_MANAGER, STRIPE_MANAGER, ZK_OPS, ISSUED_RECEIPTS_MANAGER,
 | 
				
			||||||
        BADGE_TRANSLATOR, LEVEL_TRANSLATOR);
 | 
					        BADGE_TRANSLATOR, LEVEL_TRANSLATOR);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -121,7 +138,7 @@ class SubscriptionControllerTest {
 | 
				
			||||||
  class SetSubscriptionLevel {
 | 
					  class SetSubscriptionLevel {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private final long levelId = 5L;
 | 
					    private final long levelId = 5L;
 | 
				
			||||||
    private final String currency = "eur";
 | 
					    private final String currency = "jpy";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private String subscriberId;
 | 
					    private String subscriberId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -145,15 +162,6 @@ class SubscriptionControllerTest {
 | 
				
			||||||
      when(SUBSCRIPTION_MANAGER.get(eq(Arrays.copyOfRange(subscriberUserAndKey, 0, 16)), any()))
 | 
					      when(SUBSCRIPTION_MANAGER.get(eq(Arrays.copyOfRange(subscriberUserAndKey, 0, 16)), any()))
 | 
				
			||||||
          .thenReturn(CompletableFuture.completedFuture(SubscriptionManager.GetResult.found(record)));
 | 
					          .thenReturn(CompletableFuture.completedFuture(SubscriptionManager.GetResult.found(record)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      final SubscriptionLevelConfiguration levelConfig = mock(SubscriptionLevelConfiguration.class);
 | 
					 | 
				
			||||||
      when(SUBSCRIPTION_CONFIG.getLevels())
 | 
					 | 
				
			||||||
          .thenReturn(Map.of(levelId, levelConfig));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      final SubscriptionPriceConfiguration priceConfig = new SubscriptionPriceConfiguration("testPriceId",
 | 
					 | 
				
			||||||
          BigDecimal.TEN);
 | 
					 | 
				
			||||||
      when(levelConfig.getPrices())
 | 
					 | 
				
			||||||
          .thenReturn(Map.of(currency, priceConfig));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      when(SUBSCRIPTION_MANAGER.subscriptionCreated(any(), any(), any(), anyLong()))
 | 
					      when(SUBSCRIPTION_MANAGER.subscriptionCreated(any(), any(), any(), anyLong()))
 | 
				
			||||||
          .thenReturn(CompletableFuture.completedFuture(null));
 | 
					          .thenReturn(CompletableFuture.completedFuture(null));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -364,15 +372,170 @@ class SubscriptionControllerTest {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Test
 | 
					  @Test
 | 
				
			||||||
  void getLevels() {
 | 
					  void getSubscriptionConfiguration() {
 | 
				
			||||||
    when(SUBSCRIPTION_CONFIG.getLevels()).thenReturn(Map.of(
 | 
					
 | 
				
			||||||
        1L, new SubscriptionLevelConfiguration("B1", "P1",
 | 
					    when(BADGE_TRANSLATOR.translate(any(), eq("B1"))).thenReturn(new Badge("B1", "cat1", "name1", "desc1",
 | 
				
			||||||
            Map.of("USD", new SubscriptionPriceConfiguration("R1", BigDecimal.valueOf(100)))),
 | 
					        List.of("l", "m", "h", "x", "xx", "xxx"), "SVG",
 | 
				
			||||||
        2L, new SubscriptionLevelConfiguration("B2", "P2",
 | 
					        List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld"))));
 | 
				
			||||||
            Map.of("USD", new SubscriptionPriceConfiguration("R2", BigDecimal.valueOf(200)))),
 | 
					    when(BADGE_TRANSLATOR.translate(any(), eq("B2"))).thenReturn(new Badge("B2", "cat2", "name2", "desc2",
 | 
				
			||||||
        3L, new SubscriptionLevelConfiguration("B3", "P3",
 | 
					        List.of("l", "m", "h", "x", "xx", "xxx"), "SVG",
 | 
				
			||||||
            Map.of("USD", new SubscriptionPriceConfiguration("R3", BigDecimal.valueOf(300))))
 | 
					        List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld"))));
 | 
				
			||||||
 | 
					    when(BADGE_TRANSLATOR.translate(any(), eq("B3"))).thenReturn(new Badge("B3", "cat3", "name3", "desc3",
 | 
				
			||||||
 | 
					        List.of("l", "m", "h", "x", "xx", "xxx"), "SVG",
 | 
				
			||||||
 | 
					        List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld"))));
 | 
				
			||||||
 | 
					    when(BADGE_TRANSLATOR.translate(any(), eq("BOOST"))).thenReturn(new Badge("BOOST", "boost1", "boost1", "boost1",
 | 
				
			||||||
 | 
					        List.of("l", "m", "h", "x", "xx", "xxx"), "SVG",
 | 
				
			||||||
 | 
					        List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld"))));
 | 
				
			||||||
 | 
					    when(BADGE_TRANSLATOR.translate(any(), eq("GIFT"))).thenReturn(new Badge("GIFT", "gift1", "gift1", "gift1",
 | 
				
			||||||
 | 
					        List.of("l", "m", "h", "x", "xx", "xxx"), "SVG",
 | 
				
			||||||
 | 
					        List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld"))));
 | 
				
			||||||
 | 
					    when(LEVEL_TRANSLATOR.translate(any(), eq("B1"))).thenReturn("Z1");
 | 
				
			||||||
 | 
					    when(LEVEL_TRANSLATOR.translate(any(), eq("B2"))).thenReturn("Z2");
 | 
				
			||||||
 | 
					    when(LEVEL_TRANSLATOR.translate(any(), eq("B3"))).thenReturn("Z3");
 | 
				
			||||||
 | 
					    when(LEVEL_TRANSLATOR.translate(any(), eq("BOOST"))).thenReturn("ZBOOST");
 | 
				
			||||||
 | 
					    when(LEVEL_TRANSLATOR.translate(any(), eq("GIFT"))).thenReturn("ZGIFT");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    GetSubscriptionConfigurationResponse response = RESOURCE_EXTENSION.target("/v1/subscription/configuration")
 | 
				
			||||||
 | 
					        .request()
 | 
				
			||||||
 | 
					        .get(GetSubscriptionConfigurationResponse.class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assertThat(response.currencies()).containsKeys("usd", "jpy", "bif").satisfies(currencyMap -> {
 | 
				
			||||||
 | 
					      assertThat(currencyMap).extractingByKey("usd").satisfies(currency -> {
 | 
				
			||||||
 | 
					        assertThat(currency.minimum()).isEqualByComparingTo(
 | 
				
			||||||
 | 
					            BigDecimal.valueOf(2.5).setScale(2, RoundingMode.HALF_EVEN));
 | 
				
			||||||
 | 
					        assertThat(currency.oneTime()).isEqualTo(
 | 
				
			||||||
 | 
					            Map.of("1",
 | 
				
			||||||
 | 
					                List.of(BigDecimal.valueOf(5.5).setScale(2, RoundingMode.HALF_EVEN), BigDecimal.valueOf(6),
 | 
				
			||||||
 | 
					                    BigDecimal.valueOf(7), BigDecimal.valueOf(8),
 | 
				
			||||||
 | 
					                    BigDecimal.valueOf(9), BigDecimal.valueOf(10)), "100",
 | 
				
			||||||
 | 
					                List.of(BigDecimal.valueOf(20))));
 | 
				
			||||||
 | 
					        assertThat(currency.subscription()).isEqualTo(
 | 
				
			||||||
 | 
					            Map.of("5", BigDecimal.valueOf(5), "15", BigDecimal.valueOf(15), "35", BigDecimal.valueOf(35)));
 | 
				
			||||||
 | 
					        assertThat(currency.supportedPaymentMethods()).isEqualTo(List.of("CARD"));
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      assertThat(currencyMap).extractingByKey("jpy").satisfies(currency -> {
 | 
				
			||||||
 | 
					        assertThat(currency.minimum()).isEqualByComparingTo(
 | 
				
			||||||
 | 
					            BigDecimal.valueOf(250));
 | 
				
			||||||
 | 
					        assertThat(currency.oneTime()).isEqualTo(
 | 
				
			||||||
 | 
					            Map.of("1",
 | 
				
			||||||
 | 
					                List.of(BigDecimal.valueOf(550), BigDecimal.valueOf(600),
 | 
				
			||||||
 | 
					                    BigDecimal.valueOf(700), BigDecimal.valueOf(800),
 | 
				
			||||||
 | 
					                    BigDecimal.valueOf(900), BigDecimal.valueOf(1000)), "100",
 | 
				
			||||||
 | 
					                List.of(BigDecimal.valueOf(2000))));
 | 
				
			||||||
 | 
					        assertThat(currency.subscription()).isEqualTo(
 | 
				
			||||||
 | 
					            Map.of("5", BigDecimal.valueOf(500), "15", BigDecimal.valueOf(1500), "35", BigDecimal.valueOf(3500)));
 | 
				
			||||||
 | 
					        assertThat(currency.supportedPaymentMethods()).isEqualTo(List.of("CARD"));
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      assertThat(currencyMap).extractingByKey("bif").satisfies(currency -> {
 | 
				
			||||||
 | 
					        assertThat(currency.minimum()).isEqualByComparingTo(
 | 
				
			||||||
 | 
					            BigDecimal.valueOf(2500));
 | 
				
			||||||
 | 
					        assertThat(currency.oneTime()).isEqualTo(
 | 
				
			||||||
 | 
					            Map.of("1",
 | 
				
			||||||
 | 
					                List.of(BigDecimal.valueOf(5500), BigDecimal.valueOf(6000),
 | 
				
			||||||
 | 
					                    BigDecimal.valueOf(7000), BigDecimal.valueOf(8000),
 | 
				
			||||||
 | 
					                    BigDecimal.valueOf(9000), BigDecimal.valueOf(10000)), "100",
 | 
				
			||||||
 | 
					                List.of(BigDecimal.valueOf(20000))));
 | 
				
			||||||
 | 
					        assertThat(currency.subscription()).isEqualTo(
 | 
				
			||||||
 | 
					            Map.of("5", BigDecimal.valueOf(5000), "15", BigDecimal.valueOf(15000), "35", BigDecimal.valueOf(35000)));
 | 
				
			||||||
 | 
					        assertThat(currency.supportedPaymentMethods()).isEqualTo(List.of("CARD"));
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assertThat(response.levels()).containsKeys("1", "5", "15", "35", "100").satisfies(levelsMap -> {
 | 
				
			||||||
 | 
					      assertThat(levelsMap).extractingByKey("1").satisfies(level -> {
 | 
				
			||||||
 | 
					        assertThat(level.name()).isEqualTo("ZBOOST");
 | 
				
			||||||
 | 
					        assertThat(level).extracting(SubscriptionController.LevelConfiguration::badge).satisfies(badge -> {
 | 
				
			||||||
 | 
					          assertThat(badge.getId()).isEqualTo("BOOST");
 | 
				
			||||||
 | 
					          assertThat(badge.getName()).isEqualTo("boost1");
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      assertThat(levelsMap).extractingByKey("100").satisfies(level -> {
 | 
				
			||||||
 | 
					        assertThat(level.name()).isEqualTo("ZGIFT");
 | 
				
			||||||
 | 
					        assertThat(level).extracting(SubscriptionController.LevelConfiguration::badge).satisfies(badge -> {
 | 
				
			||||||
 | 
					          assertThat(badge.getId()).isEqualTo("GIFT");
 | 
				
			||||||
 | 
					          assertThat(badge.getName()).isEqualTo("gift1");
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      assertThat(levelsMap).extractingByKey("5").satisfies(level -> {
 | 
				
			||||||
 | 
					        assertThat(level.name()).isEqualTo("Z1");
 | 
				
			||||||
 | 
					        assertThat(level).extracting(SubscriptionController.LevelConfiguration::badge).satisfies(badge -> {
 | 
				
			||||||
 | 
					          assertThat(badge.getId()).isEqualTo("B1");
 | 
				
			||||||
 | 
					          assertThat(badge.getName()).isEqualTo("name1");
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      assertThat(levelsMap).extractingByKey("15").satisfies(level -> {
 | 
				
			||||||
 | 
					        assertThat(level.name()).isEqualTo("Z2");
 | 
				
			||||||
 | 
					        assertThat(level).extracting(SubscriptionController.LevelConfiguration::badge).satisfies(badge -> {
 | 
				
			||||||
 | 
					          assertThat(badge.getId()).isEqualTo("B2");
 | 
				
			||||||
 | 
					          assertThat(badge.getName()).isEqualTo("name2");
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      assertThat(levelsMap).extractingByKey("35").satisfies(level -> {
 | 
				
			||||||
 | 
					        assertThat(level.name()).isEqualTo("Z3");
 | 
				
			||||||
 | 
					        assertThat(level).extracting(SubscriptionController.LevelConfiguration::badge).satisfies(badge -> {
 | 
				
			||||||
 | 
					          assertThat(badge.getId()).isEqualTo("B3");
 | 
				
			||||||
 | 
					          assertThat(badge.getName()).isEqualTo("name3");
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // check the badge vs purchasable badge fields
 | 
				
			||||||
 | 
					    // subscription levels are Badge, while one-time levels are PurchasableBadge, which adds `duration`
 | 
				
			||||||
 | 
					    Map<String, Object> genericResponse = RESOURCE_EXTENSION.target("/v1/subscription/configuration")
 | 
				
			||||||
 | 
					        .request()
 | 
				
			||||||
 | 
					        .get(Map.class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assertThat(genericResponse.get("levels")).satisfies(levels -> {
 | 
				
			||||||
 | 
					      final Set<String> oneTimeLevels = Set.of("1", "100");
 | 
				
			||||||
 | 
					      oneTimeLevels.forEach(oneTimeLevel -> {
 | 
				
			||||||
 | 
					        assertThat((Map<String, Map<String, Map<String, Object>>>) levels).extractingByKey(oneTimeLevel)
 | 
				
			||||||
 | 
					            .satisfies(level -> {
 | 
				
			||||||
 | 
					              assertThat(level.get("badge")).containsKeys("duration");
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      ((Map<String, ?>) levels).keySet().stream()
 | 
				
			||||||
 | 
					          .filter(Predicate.not(oneTimeLevels::contains))
 | 
				
			||||||
 | 
					          .forEach(subscriptionLevel -> {
 | 
				
			||||||
 | 
					            assertThat((Map<String, Map<String, Map<String, Object>>>) levels).extractingByKey(subscriptionLevel)
 | 
				
			||||||
 | 
					                .satisfies(level -> {
 | 
				
			||||||
 | 
					                  assertThat(level.get("badge")).doesNotContainKeys("duration");
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Test
 | 
				
			||||||
 | 
					  void testGetBoostAmounts() {
 | 
				
			||||||
 | 
					    final Map<?, ?> boostAmounts = RESOURCE_EXTENSION.target("/v1/subscription/boost/amounts")
 | 
				
			||||||
 | 
					        .request()
 | 
				
			||||||
 | 
					        .get(Map.class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assertThat(boostAmounts).isEqualTo(Map.of(
 | 
				
			||||||
 | 
					        "USD", List.of(5.50, 6, 7, 8, 9, 10),
 | 
				
			||||||
 | 
					        "JPY", List.of(550, 600, 700, 800, 900, 1000),
 | 
				
			||||||
 | 
					        "BIF", List.of(5500, 6000, 7000, 8000, 9000, 10000)
 | 
				
			||||||
    ));
 | 
					    ));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final Map<?, ?> giftAmounts = RESOURCE_EXTENSION.target("/v1/subscription/boost/amounts/gift")
 | 
				
			||||||
 | 
					        .request()
 | 
				
			||||||
 | 
					        .get(Map.class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assertThat(giftAmounts).isEqualTo(Map.of(
 | 
				
			||||||
 | 
					        "USD", 20,
 | 
				
			||||||
 | 
					        "JPY", 2000,
 | 
				
			||||||
 | 
					        "BIF", 20000
 | 
				
			||||||
 | 
					    ));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Test
 | 
				
			||||||
 | 
					  void getLevels() {
 | 
				
			||||||
    when(BADGE_TRANSLATOR.translate(any(), eq("B1"))).thenReturn(new Badge("B1", "cat1", "name1", "desc1",
 | 
					    when(BADGE_TRANSLATOR.translate(any(), eq("B1"))).thenReturn(new Badge("B1", "cat1", "name1", "desc1",
 | 
				
			||||||
        List.of("l", "m", "h", "x", "xx", "xxx"), "SVG",
 | 
					        List.of("l", "m", "h", "x", "xx", "xxx"), "SVG",
 | 
				
			||||||
        List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld"))));
 | 
					        List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld"))));
 | 
				
			||||||
| 
						 | 
					@ -390,28 +553,136 @@ class SubscriptionControllerTest {
 | 
				
			||||||
        .request()
 | 
					        .request()
 | 
				
			||||||
        .get(GetLevelsResponse.class);
 | 
					        .get(GetLevelsResponse.class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    assertThat(response.getLevels()).containsKeys(1L, 2L, 3L).satisfies(longLevelMap -> {
 | 
					    assertThat(response.getLevels()).containsKeys(5L, 15L, 35L).satisfies(longLevelMap -> {
 | 
				
			||||||
      assertThat(longLevelMap).extractingByKey(1L).satisfies(level -> {
 | 
					      assertThat(longLevelMap).extractingByKey(5L).satisfies(level -> {
 | 
				
			||||||
        assertThat(level.getName()).isEqualTo("Z1");
 | 
					        assertThat(level.getName()).isEqualTo("Z1");
 | 
				
			||||||
        assertThat(level.getBadge().getId()).isEqualTo("B1");
 | 
					        assertThat(level.getBadge().getId()).isEqualTo("B1");
 | 
				
			||||||
        assertThat(level.getCurrencies()).containsKeys("USD").extractingByKey("USD").satisfies(price -> {
 | 
					        assertThat(level.getCurrencies()).containsKeys("USD").extractingByKey("USD").satisfies(price -> {
 | 
				
			||||||
          assertThat(price).isEqualTo("100");
 | 
					          assertThat(price).isEqualTo("5");
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      assertThat(longLevelMap).extractingByKey(2L).satisfies(level -> {
 | 
					      assertThat(longLevelMap).extractingByKey(15L).satisfies(level -> {
 | 
				
			||||||
        assertThat(level.getName()).isEqualTo("Z2");
 | 
					        assertThat(level.getName()).isEqualTo("Z2");
 | 
				
			||||||
        assertThat(level.getBadge().getId()).isEqualTo("B2");
 | 
					        assertThat(level.getBadge().getId()).isEqualTo("B2");
 | 
				
			||||||
        assertThat(level.getCurrencies()).containsKeys("USD").extractingByKey("USD").satisfies(price -> {
 | 
					        assertThat(level.getCurrencies()).containsKeys("USD").extractingByKey("USD").satisfies(price -> {
 | 
				
			||||||
          assertThat(price).isEqualTo("200");
 | 
					          assertThat(price).isEqualTo("15");
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      assertThat(longLevelMap).extractingByKey(3L).satisfies(level -> {
 | 
					      assertThat(longLevelMap).extractingByKey(35L).satisfies(level -> {
 | 
				
			||||||
        assertThat(level.getName()).isEqualTo("Z3");
 | 
					        assertThat(level.getName()).isEqualTo("Z3");
 | 
				
			||||||
        assertThat(level.getBadge().getId()).isEqualTo("B3");
 | 
					        assertThat(level.getBadge().getId()).isEqualTo("B3");
 | 
				
			||||||
        assertThat(level.getCurrencies()).containsKeys("USD").extractingByKey("USD").satisfies(price -> {
 | 
					        assertThat(level.getCurrencies()).containsKeys("USD").extractingByKey("USD").satisfies(price -> {
 | 
				
			||||||
          assertThat(price).isEqualTo("300");
 | 
					          assertThat(price).isEqualTo("35");
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Encapsulates {@code static} configuration, to keep the class header simpler and avoid illegal forward references
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  private record ConfigHelper() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static SubscriptionConfiguration getSubscriptionConfig() {
 | 
				
			||||||
 | 
					      return readValue(SUBSCRIPTION_CONFIG_YAML, SubscriptionConfiguration.class);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static OneTimeDonationConfiguration getOneTimeConfig() {
 | 
				
			||||||
 | 
					      return readValue(ONETIME_CONFIG_YAML, OneTimeDonationConfiguration.class);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static <T> T readValue(String yaml, Class<T> type) {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        return YAML_MAPPER.readValue(yaml, type);
 | 
				
			||||||
 | 
					      } catch (Exception e) {
 | 
				
			||||||
 | 
					        throw new RuntimeException(e);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static final String SUBSCRIPTION_CONFIG_YAML = """
 | 
				
			||||||
 | 
					        badgeGracePeriod: P15D
 | 
				
			||||||
 | 
					        levels:
 | 
				
			||||||
 | 
					          5:
 | 
				
			||||||
 | 
					            badge: B1
 | 
				
			||||||
 | 
					            prices:
 | 
				
			||||||
 | 
					              usd:
 | 
				
			||||||
 | 
					                amount: '5'
 | 
				
			||||||
 | 
					                id: R1
 | 
				
			||||||
 | 
					              jpy:
 | 
				
			||||||
 | 
					                amount: '500'
 | 
				
			||||||
 | 
					                id: Q1
 | 
				
			||||||
 | 
					              bif:
 | 
				
			||||||
 | 
					                amount: '5000'
 | 
				
			||||||
 | 
					                id: S1
 | 
				
			||||||
 | 
					          15:
 | 
				
			||||||
 | 
					            badge: B2
 | 
				
			||||||
 | 
					            prices:
 | 
				
			||||||
 | 
					              usd:
 | 
				
			||||||
 | 
					                amount: '15'
 | 
				
			||||||
 | 
					                id: R2
 | 
				
			||||||
 | 
					              jpy:
 | 
				
			||||||
 | 
					                amount: '1500'
 | 
				
			||||||
 | 
					                id: Q2
 | 
				
			||||||
 | 
					              bif:
 | 
				
			||||||
 | 
					                amount: '15000'
 | 
				
			||||||
 | 
					                id: S2
 | 
				
			||||||
 | 
					          35:
 | 
				
			||||||
 | 
					            badge: B3
 | 
				
			||||||
 | 
					            prices:
 | 
				
			||||||
 | 
					              usd:
 | 
				
			||||||
 | 
					                amount: '35'
 | 
				
			||||||
 | 
					                id: R3
 | 
				
			||||||
 | 
					              jpy:
 | 
				
			||||||
 | 
					                amount: '3500'
 | 
				
			||||||
 | 
					                id: Q3
 | 
				
			||||||
 | 
					              bif:
 | 
				
			||||||
 | 
					                amount: '35000'
 | 
				
			||||||
 | 
					                id: S3
 | 
				
			||||||
 | 
					        """;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static final String ONETIME_CONFIG_YAML = """
 | 
				
			||||||
 | 
					        boost:
 | 
				
			||||||
 | 
					          level: 1
 | 
				
			||||||
 | 
					          expiration: P45D
 | 
				
			||||||
 | 
					          badge: BOOST
 | 
				
			||||||
 | 
					        gift:
 | 
				
			||||||
 | 
					          level: 100
 | 
				
			||||||
 | 
					          expiration: P60D
 | 
				
			||||||
 | 
					          badge: GIFT
 | 
				
			||||||
 | 
					        currencies:
 | 
				
			||||||
 | 
					          usd:
 | 
				
			||||||
 | 
					            minimum: '2.50'
 | 
				
			||||||
 | 
					            gift: '20'
 | 
				
			||||||
 | 
					            boosts:
 | 
				
			||||||
 | 
					              - '5.50'
 | 
				
			||||||
 | 
					              - '6'
 | 
				
			||||||
 | 
					              - '7'
 | 
				
			||||||
 | 
					              - '8'
 | 
				
			||||||
 | 
					              - '9'
 | 
				
			||||||
 | 
					              - '10'
 | 
				
			||||||
 | 
					          jpy:
 | 
				
			||||||
 | 
					            minimum: '250'
 | 
				
			||||||
 | 
					            gift: '2000'
 | 
				
			||||||
 | 
					            boosts:
 | 
				
			||||||
 | 
					              - '550'
 | 
				
			||||||
 | 
					              - '600'
 | 
				
			||||||
 | 
					              - '700'
 | 
				
			||||||
 | 
					              - '800'
 | 
				
			||||||
 | 
					              - '900'
 | 
				
			||||||
 | 
					              - '1000'
 | 
				
			||||||
 | 
					          bif:
 | 
				
			||||||
 | 
					            minimum: '2500'
 | 
				
			||||||
 | 
					            gift: '20000'
 | 
				
			||||||
 | 
					            boosts:
 | 
				
			||||||
 | 
					              - '5500'
 | 
				
			||||||
 | 
					              - '6000'
 | 
				
			||||||
 | 
					              - '7000'
 | 
				
			||||||
 | 
					              - '8000'
 | 
				
			||||||
 | 
					              - '9000'
 | 
				
			||||||
 | 
					              - '10000'
 | 
				
			||||||
 | 
					        """;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue