Add backup levels to subscription configuration response
This commit is contained in:
		
							parent
							
								
									44ad9d4f5f
								
							
						
					
					
						commit
						4863e1d227
					
				| 
						 | 
					@ -8,11 +8,15 @@ package org.whispersystems.textsecuregcm.configuration;
 | 
				
			||||||
import com.fasterxml.jackson.annotation.JsonCreator;
 | 
					import com.fasterxml.jackson.annotation.JsonCreator;
 | 
				
			||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
 | 
					import com.fasterxml.jackson.annotation.JsonIgnore;
 | 
				
			||||||
import com.fasterxml.jackson.annotation.JsonProperty;
 | 
					import com.fasterxml.jackson.annotation.JsonProperty;
 | 
				
			||||||
 | 
					import com.google.common.collect.Sets;
 | 
				
			||||||
import io.dropwizard.validation.ValidationMethod;
 | 
					import io.dropwizard.validation.ValidationMethod;
 | 
				
			||||||
import java.time.Duration;
 | 
					import java.time.Duration;
 | 
				
			||||||
 | 
					import java.util.Collections;
 | 
				
			||||||
import java.util.Map;
 | 
					import java.util.Map;
 | 
				
			||||||
import java.util.Optional;
 | 
					import java.util.Optional;
 | 
				
			||||||
import java.util.Set;
 | 
					import java.util.Set;
 | 
				
			||||||
 | 
					import java.util.stream.Collectors;
 | 
				
			||||||
 | 
					import java.util.stream.Stream;
 | 
				
			||||||
import javax.validation.Valid;
 | 
					import javax.validation.Valid;
 | 
				
			||||||
import javax.validation.constraints.Min;
 | 
					import javax.validation.constraints.Min;
 | 
				
			||||||
import javax.validation.constraints.NotNull;
 | 
					import javax.validation.constraints.NotNull;
 | 
				
			||||||
| 
						 | 
					@ -21,16 +25,23 @@ public class SubscriptionConfiguration {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private final Duration badgeGracePeriod;
 | 
					  private final Duration badgeGracePeriod;
 | 
				
			||||||
  private final Duration badgeExpiration;
 | 
					  private final Duration badgeExpiration;
 | 
				
			||||||
  private final Map<Long, SubscriptionLevelConfiguration> levels;
 | 
					
 | 
				
			||||||
 | 
					  private final Duration backupExpiration;
 | 
				
			||||||
 | 
					  private final Map<Long, SubscriptionLevelConfiguration.Donation> donationLevels;
 | 
				
			||||||
 | 
					  private final Map<Long, SubscriptionLevelConfiguration.Backup> backupLevels;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @JsonCreator
 | 
					  @JsonCreator
 | 
				
			||||||
  public SubscriptionConfiguration(
 | 
					  public SubscriptionConfiguration(
 | 
				
			||||||
      @JsonProperty("badgeGracePeriod") @Valid Duration badgeGracePeriod,
 | 
					      @JsonProperty("badgeGracePeriod") @Valid Duration badgeGracePeriod,
 | 
				
			||||||
      @JsonProperty("badgeExpiration") @Valid Duration badgeExpiration,
 | 
					      @JsonProperty("badgeExpiration") @Valid Duration badgeExpiration,
 | 
				
			||||||
      @JsonProperty("levels") @Valid Map<@NotNull @Min(1) Long, @NotNull @Valid SubscriptionLevelConfiguration> levels) {
 | 
					      @JsonProperty("backupExpiration") @Valid Duration backupExpiration,
 | 
				
			||||||
 | 
					      @JsonProperty("levels") @Valid Map<@NotNull @Min(1) Long, SubscriptionLevelConfiguration.@NotNull @Valid Donation> donationLevels,
 | 
				
			||||||
 | 
					      @JsonProperty("backupLevels") @Valid Map<@NotNull @Min(1) Long, SubscriptionLevelConfiguration.@NotNull @Valid Backup> backupLevels) {
 | 
				
			||||||
    this.badgeGracePeriod = badgeGracePeriod;
 | 
					    this.badgeGracePeriod = badgeGracePeriod;
 | 
				
			||||||
    this.badgeExpiration = badgeExpiration;
 | 
					    this.badgeExpiration = badgeExpiration;
 | 
				
			||||||
    this.levels = levels;
 | 
					    this.donationLevels = donationLevels;
 | 
				
			||||||
 | 
					    this.backupExpiration = backupExpiration;
 | 
				
			||||||
 | 
					    this.backupLevels = backupLevels == null ? Collections.emptyMap() : backupLevels;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public Duration getBadgeGracePeriod() {
 | 
					  public Duration getBadgeGracePeriod() {
 | 
				
			||||||
| 
						 | 
					@ -42,19 +53,43 @@ public class SubscriptionConfiguration {
 | 
				
			||||||
    return badgeExpiration;
 | 
					    return badgeExpiration;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public Map<Long, SubscriptionLevelConfiguration> getLevels() {
 | 
					  public Duration getBackupExpiration() {
 | 
				
			||||||
    return levels;
 | 
					    return backupExpiration;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public SubscriptionLevelConfiguration getSubscriptionLevel(long level) {
 | 
				
			||||||
 | 
					    return Optional
 | 
				
			||||||
 | 
					        .<SubscriptionLevelConfiguration>ofNullable(this.donationLevels.get(level))
 | 
				
			||||||
 | 
					        .orElse(this.backupLevels.get(level));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public Map<Long, SubscriptionLevelConfiguration.Donation> getDonationLevels() {
 | 
				
			||||||
 | 
					    return donationLevels;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public Map<Long, SubscriptionLevelConfiguration.Backup> getBackupLevels() {
 | 
				
			||||||
 | 
					    return backupLevels;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @JsonIgnore
 | 
				
			||||||
 | 
					  @ValidationMethod(message = "Backup levels and donation levels should not contain the same level identifier")
 | 
				
			||||||
 | 
					  public boolean areLevelsNonOverlapping() {
 | 
				
			||||||
 | 
					    return Sets.intersection(backupLevels.keySet(), donationLevels.keySet()).isEmpty();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @JsonIgnore
 | 
					  @JsonIgnore
 | 
				
			||||||
  @ValidationMethod(message = "has a mismatch between the levels supported currencies")
 | 
					  @ValidationMethod(message = "has a mismatch between the levels supported currencies")
 | 
				
			||||||
  public boolean isCurrencyListSameAcrossAllLevels() {
 | 
					  public boolean isCurrencyListSameAcrossAllLevels() {
 | 
				
			||||||
    Optional<SubscriptionLevelConfiguration> any = levels.values().stream().findAny();
 | 
					    final Map<Long, SubscriptionLevelConfiguration> subscriptionLevels = Stream
 | 
				
			||||||
 | 
					        .concat(donationLevels.entrySet().stream(), backupLevels.entrySet().stream())
 | 
				
			||||||
 | 
					        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Optional<SubscriptionLevelConfiguration> any = subscriptionLevels.values().stream().findAny();
 | 
				
			||||||
    if (any.isEmpty()) {
 | 
					    if (any.isEmpty()) {
 | 
				
			||||||
      return true;
 | 
					      return true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Set<String> currencies = any.get().getPrices().keySet();
 | 
					    Set<String> currencies = any.get().prices().keySet();
 | 
				
			||||||
    return levels.values().stream().allMatch(level -> currencies.equals(level.getPrices().keySet()));
 | 
					    return subscriptionLevels.values().stream().allMatch(level -> currencies.equals(level.prices().keySet()));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,31 +5,35 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
package org.whispersystems.textsecuregcm.configuration;
 | 
					package org.whispersystems.textsecuregcm.configuration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import com.fasterxml.jackson.annotation.JsonCreator;
 | 
					 | 
				
			||||||
import com.fasterxml.jackson.annotation.JsonProperty;
 | 
					import com.fasterxml.jackson.annotation.JsonProperty;
 | 
				
			||||||
import java.util.Map;
 | 
					import java.util.Map;
 | 
				
			||||||
import javax.validation.Valid;
 | 
					import javax.validation.Valid;
 | 
				
			||||||
import javax.validation.constraints.NotEmpty;
 | 
					import javax.validation.constraints.NotEmpty;
 | 
				
			||||||
import javax.validation.constraints.NotNull;
 | 
					import javax.validation.constraints.NotNull;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public class SubscriptionLevelConfiguration {
 | 
					public sealed interface SubscriptionLevelConfiguration permits
 | 
				
			||||||
 | 
					    SubscriptionLevelConfiguration.Backup, SubscriptionLevelConfiguration.Donation {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private final String badge;
 | 
					  Map<String, SubscriptionPriceConfiguration> prices();
 | 
				
			||||||
  private final Map<String, SubscriptionPriceConfiguration> prices;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @JsonCreator
 | 
					  enum Type {
 | 
				
			||||||
  public SubscriptionLevelConfiguration(
 | 
					    DONATION,
 | 
				
			||||||
 | 
					    BACKUP
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  default Type type() {
 | 
				
			||||||
 | 
					    return switch (this) {
 | 
				
			||||||
 | 
					      case Backup b -> Type.BACKUP;
 | 
				
			||||||
 | 
					      case Donation d -> Type.DONATION;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  record Backup(
 | 
				
			||||||
 | 
					      @JsonProperty("prices") @Valid Map<@NotEmpty String, @NotNull @Valid SubscriptionPriceConfiguration> prices)
 | 
				
			||||||
 | 
					      implements SubscriptionLevelConfiguration {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  record Donation(
 | 
				
			||||||
      @JsonProperty("badge") @NotEmpty String badge,
 | 
					      @JsonProperty("badge") @NotEmpty String badge,
 | 
				
			||||||
      @JsonProperty("prices") @Valid Map<@NotEmpty String, @NotNull @Valid SubscriptionPriceConfiguration> prices) {
 | 
					      @JsonProperty("prices") @Valid Map<@NotEmpty String, @NotNull @Valid SubscriptionPriceConfiguration> prices)
 | 
				
			||||||
    this.badge = badge;
 | 
					      implements SubscriptionLevelConfiguration {}
 | 
				
			||||||
    this.prices = prices;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  public String getBadge() {
 | 
					 | 
				
			||||||
    return badge;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  public Map<String, SubscriptionPriceConfiguration> getPrices() {
 | 
					 | 
				
			||||||
    return prices;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -33,6 +33,7 @@ import java.util.Objects;
 | 
				
			||||||
import java.util.Optional;
 | 
					import java.util.Optional;
 | 
				
			||||||
import java.util.concurrent.CompletableFuture;
 | 
					import java.util.concurrent.CompletableFuture;
 | 
				
			||||||
import java.util.concurrent.CompletionException;
 | 
					import java.util.concurrent.CompletionException;
 | 
				
			||||||
 | 
					import java.util.function.Function;
 | 
				
			||||||
import java.util.stream.Collectors;
 | 
					import java.util.stream.Collectors;
 | 
				
			||||||
import javax.annotation.Nonnull;
 | 
					import javax.annotation.Nonnull;
 | 
				
			||||||
import javax.crypto.Mac;
 | 
					import javax.crypto.Mac;
 | 
				
			||||||
| 
						 | 
					@ -123,6 +124,7 @@ public class SubscriptionController {
 | 
				
			||||||
  private static final String RECEIPT_ISSUED_COUNTER_NAME = MetricsUtil.name(SubscriptionController.class, "receiptIssued");
 | 
					  private static final String RECEIPT_ISSUED_COUNTER_NAME = MetricsUtil.name(SubscriptionController.class, "receiptIssued");
 | 
				
			||||||
  private static final String PROCESSOR_TAG_NAME = "processor";
 | 
					  private static final String PROCESSOR_TAG_NAME = "processor";
 | 
				
			||||||
  private static final String TYPE_TAG_NAME = "type";
 | 
					  private static final String TYPE_TAG_NAME = "type";
 | 
				
			||||||
 | 
					  private static final String SUBSCRIPTION_TYPE_TAG_NAME = "subscriptionType";
 | 
				
			||||||
  private static final String EURO_CURRENCY_CODE = "EUR";
 | 
					  private static final String EURO_CURRENCY_CODE = "EUR";
 | 
				
			||||||
  private static final Semver LAST_PROBLEMATIC_IOS_VERSION = new Semver("6.44.0");
 | 
					  private static final Semver LAST_PROBLEMATIC_IOS_VERSION = new Semver("6.44.0");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -166,12 +168,12 @@ public class SubscriptionController {
 | 
				
			||||||
              String.valueOf(oneTimeDonationConfiguration.gift().level()), List.of(currencyConfig.gift())
 | 
					              String.valueOf(oneTimeDonationConfiguration.gift().level()), List.of(currencyConfig.gift())
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          final Map<String, BigDecimal> subscriptionLevelsToAmounts = subscriptionConfiguration.getLevels()
 | 
					          final Function<Map<Long, ? extends SubscriptionLevelConfiguration>, Map<String, BigDecimal>> extractSubscriptionAmounts = levels ->
 | 
				
			||||||
              .entrySet().stream()
 | 
					              levels.entrySet().stream()
 | 
				
			||||||
              .filter(levelIdAndConfig -> levelIdAndConfig.getValue().getPrices().containsKey(currency))
 | 
					                  .filter(levelIdAndConfig -> levelIdAndConfig.getValue().prices().containsKey(currency))
 | 
				
			||||||
              .collect(Collectors.toMap(
 | 
					                  .collect(Collectors.toMap(
 | 
				
			||||||
                  levelIdAndConfig -> String.valueOf(levelIdAndConfig.getKey()),
 | 
					                      levelIdAndConfig -> String.valueOf(levelIdAndConfig.getKey()),
 | 
				
			||||||
                  levelIdAndConfig -> levelIdAndConfig.getValue().getPrices().get(currency).amount()));
 | 
					                      levelIdAndConfig -> levelIdAndConfig.getValue().prices().get(currency).amount()));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          final List<String> supportedPaymentMethods = Arrays.stream(PaymentMethod.values())
 | 
					          final List<String> supportedPaymentMethods = Arrays.stream(PaymentMethod.values())
 | 
				
			||||||
              .filter(paymentMethod -> subscriptionProcessorManagers.stream()
 | 
					              .filter(paymentMethod -> subscriptionProcessorManagers.stream()
 | 
				
			||||||
| 
						 | 
					@ -184,19 +186,24 @@ public class SubscriptionController {
 | 
				
			||||||
            throw new RuntimeException("Configuration has currency with no processor support: " + currency);
 | 
					            throw new RuntimeException("Configuration has currency with no processor support: " + currency);
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          return new CurrencyConfiguration(currencyConfig.minimum(), oneTimeLevelsToSuggestedAmounts,
 | 
					          return new CurrencyConfiguration(
 | 
				
			||||||
              subscriptionLevelsToAmounts, supportedPaymentMethods);
 | 
					              currencyConfig.minimum(),
 | 
				
			||||||
 | 
					              oneTimeLevelsToSuggestedAmounts,
 | 
				
			||||||
 | 
					              extractSubscriptionAmounts.apply(subscriptionConfiguration.getDonationLevels()),
 | 
				
			||||||
 | 
					              extractSubscriptionAmounts.apply(subscriptionConfiguration.getBackupLevels()),
 | 
				
			||||||
 | 
					              supportedPaymentMethods);
 | 
				
			||||||
        }));
 | 
					        }));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @VisibleForTesting
 | 
					  @VisibleForTesting
 | 
				
			||||||
  GetSubscriptionConfigurationResponse buildGetSubscriptionConfigurationResponse(final List<Locale> acceptableLanguages) {
 | 
					  GetSubscriptionConfigurationResponse buildGetSubscriptionConfigurationResponse(
 | 
				
			||||||
 | 
					      final List<Locale> acceptableLanguages) {
 | 
				
			||||||
    final Map<String, LevelConfiguration> levels = new HashMap<>();
 | 
					    final Map<String, LevelConfiguration> levels = new HashMap<>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    subscriptionConfiguration.getLevels().forEach((levelId, levelConfig) -> {
 | 
					    subscriptionConfiguration.getDonationLevels().forEach((levelId, levelConfig) -> {
 | 
				
			||||||
      final LevelConfiguration levelConfiguration = new LevelConfiguration(
 | 
					      final LevelConfiguration levelConfiguration = new LevelConfiguration(
 | 
				
			||||||
          levelTranslator.translate(acceptableLanguages, levelConfig.getBadge()),
 | 
					          levelTranslator.translate(acceptableLanguages, levelConfig.badge()),
 | 
				
			||||||
          badgeTranslator.translate(acceptableLanguages, levelConfig.getBadge()));
 | 
					          badgeTranslator.translate(acceptableLanguages, levelConfig.badge()));
 | 
				
			||||||
      levels.put(String.valueOf(levelId), levelConfiguration);
 | 
					      levels.put(String.valueOf(levelId), levelConfiguration);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -478,6 +485,13 @@ public class SubscriptionController {
 | 
				
			||||||
                          currency.toLowerCase(Locale.ROOT)))) {
 | 
					                          currency.toLowerCase(Locale.ROOT)))) {
 | 
				
			||||||
                        return CompletableFuture.completedFuture(subscription);
 | 
					                        return CompletableFuture.completedFuture(subscription);
 | 
				
			||||||
                      }
 | 
					                      }
 | 
				
			||||||
 | 
					                      if (!subscriptionsAreSameType(existingLevelAndCurrency.level(), level)) {
 | 
				
			||||||
 | 
					                        throw new BadRequestException(Response.status(Status.BAD_REQUEST)
 | 
				
			||||||
 | 
					                            .entity(new SetSubscriptionLevelErrorResponse(List.of(
 | 
				
			||||||
 | 
					                                new SetSubscriptionLevelErrorResponse.Error(
 | 
				
			||||||
 | 
					                                    SetSubscriptionLevelErrorResponse.Error.Type.UNSUPPORTED_LEVEL, null))))
 | 
				
			||||||
 | 
					                            .build());
 | 
				
			||||||
 | 
					                      }
 | 
				
			||||||
                      return manager.updateSubscription(
 | 
					                      return manager.updateSubscription(
 | 
				
			||||||
                              subscription, subscriptionTemplateId, level, idempotencyKey)
 | 
					                              subscription, subscriptionTemplateId, level, idempotencyKey)
 | 
				
			||||||
                          .thenCompose(updatedSubscription ->
 | 
					                          .thenCompose(updatedSubscription ->
 | 
				
			||||||
| 
						 | 
					@ -519,6 +533,11 @@ public class SubscriptionController {
 | 
				
			||||||
            .thenApply(unused -> Response.ok(new SetSubscriptionLevelSuccessResponse(level)).build());
 | 
					            .thenApply(unused -> Response.ok(new SetSubscriptionLevelSuccessResponse(level)).build());
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public boolean subscriptionsAreSameType(long level1, long level2) {
 | 
				
			||||||
 | 
					    return subscriptionConfiguration.getSubscriptionLevel(level1).type()
 | 
				
			||||||
 | 
					        == subscriptionConfiguration.getSubscriptionLevel(level2).type();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Comprehensive configuration for subscriptions and one-time donations
 | 
					   * Comprehensive configuration for subscriptions and one-time donations
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
| 
						 | 
					@ -538,10 +557,12 @@ public class SubscriptionController {
 | 
				
			||||||
   * @param oneTime                 map of numeric one-time donation level IDs to the list of default amounts to be
 | 
					   * @param oneTime                 map of numeric one-time donation level IDs to the list of default amounts to be
 | 
				
			||||||
   *                                presented
 | 
					   *                                presented
 | 
				
			||||||
   * @param subscription            map of numeric subscription level IDs to the amount charged for that level
 | 
					   * @param subscription            map of numeric subscription level IDs to the amount charged for that level
 | 
				
			||||||
 | 
					   * @param backupSubscription      map of numeric backup level IDs to the amount charged for that level
 | 
				
			||||||
   * @param supportedPaymentMethods the payment methods that support the given currency
 | 
					   * @param supportedPaymentMethods the payment methods that support the given currency
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  public record CurrencyConfiguration(BigDecimal minimum, Map<String, List<BigDecimal>> oneTime,
 | 
					  public record CurrencyConfiguration(BigDecimal minimum, Map<String, List<BigDecimal>> oneTime,
 | 
				
			||||||
                                      Map<String, BigDecimal> subscription,
 | 
					                                      Map<String, BigDecimal> subscription,
 | 
				
			||||||
 | 
					                                      Map<String, BigDecimal> backupSubscription,
 | 
				
			||||||
                                      List<String> supportedPaymentMethods) {
 | 
					                                      List<String> supportedPaymentMethods) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -946,7 +967,8 @@ public class SubscriptionController {
 | 
				
			||||||
                      try {
 | 
					                      try {
 | 
				
			||||||
                        receiptCredentialResponse = zkReceiptOperations.issueReceiptCredential(
 | 
					                        receiptCredentialResponse = zkReceiptOperations.issueReceiptCredential(
 | 
				
			||||||
                            receiptCredentialRequest,
 | 
					                            receiptCredentialRequest,
 | 
				
			||||||
                            receiptExpirationWithGracePeriod(receipt.paidAt()).getEpochSecond(), receipt.level());
 | 
					                            receiptExpirationWithGracePeriod(receipt.paidAt(), receipt.level()).getEpochSecond(),
 | 
				
			||||||
 | 
					                            receipt.level());
 | 
				
			||||||
                      } catch (VerificationFailedException e) {
 | 
					                      } catch (VerificationFailedException e) {
 | 
				
			||||||
                        throw new BadRequestException("receipt credential request failed verification", e);
 | 
					                        throw new BadRequestException("receipt credential request failed verification", e);
 | 
				
			||||||
                      }
 | 
					                      }
 | 
				
			||||||
| 
						 | 
					@ -954,6 +976,9 @@ public class SubscriptionController {
 | 
				
			||||||
                              Tags.of(
 | 
					                              Tags.of(
 | 
				
			||||||
                                  Tag.of(PROCESSOR_TAG_NAME, manager.getProcessor().toString()),
 | 
					                                  Tag.of(PROCESSOR_TAG_NAME, manager.getProcessor().toString()),
 | 
				
			||||||
                                  Tag.of(TYPE_TAG_NAME, "subscription"),
 | 
					                                  Tag.of(TYPE_TAG_NAME, "subscription"),
 | 
				
			||||||
 | 
					                                  Tag.of(SUBSCRIPTION_TYPE_TAG_NAME,
 | 
				
			||||||
 | 
					                                      subscriptionConfiguration.getSubscriptionLevel(receipt.level()).type().name()
 | 
				
			||||||
 | 
					                                          .toLowerCase(Locale.ROOT)),
 | 
				
			||||||
                                  UserAgentTagUtil.getPlatformTag(userAgent)))
 | 
					                                  UserAgentTagUtil.getPlatformTag(userAgent)))
 | 
				
			||||||
                          .increment();
 | 
					                          .increment();
 | 
				
			||||||
                      return Response.ok(new GetReceiptCredentialsResponse(receiptCredentialResponse.serialize()))
 | 
					                      return Response.ok(new GetReceiptCredentialsResponse(receiptCredentialResponse.serialize()))
 | 
				
			||||||
| 
						 | 
					@ -989,32 +1014,38 @@ public class SubscriptionController {
 | 
				
			||||||
                new ClientErrorException(Status.CONFLICT)))
 | 
					                new ClientErrorException(Status.CONFLICT)))
 | 
				
			||||||
        .thenApply(customer -> Response.ok().build());
 | 
					        .thenApply(customer -> Response.ok().build());
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
    private Instant receiptExpirationWithGracePeriod(Instant paidAt) {
 | 
					 | 
				
			||||||
        return paidAt.plus(subscriptionConfiguration.getBadgeExpiration())
 | 
					 | 
				
			||||||
            .plus(subscriptionConfiguration.getBadgeGracePeriod())
 | 
					 | 
				
			||||||
            .truncatedTo(ChronoUnit.DAYS)
 | 
					 | 
				
			||||||
            .plus(1, ChronoUnit.DAYS);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private String getSubscriptionTemplateId(long level, String currency, SubscriptionProcessor processor) {
 | 
					  private Instant receiptExpirationWithGracePeriod(Instant paidAt, long level) {
 | 
				
			||||||
      SubscriptionLevelConfiguration levelConfiguration = subscriptionConfiguration.getLevels().get(level);
 | 
					    return switch (subscriptionConfiguration.getSubscriptionLevel(level).type()) {
 | 
				
			||||||
      if (levelConfiguration == null) {
 | 
					      case DONATION -> paidAt.plus(subscriptionConfiguration.getBadgeExpiration())
 | 
				
			||||||
        throw new BadRequestException(Response.status(Status.BAD_REQUEST)
 | 
					          .plus(subscriptionConfiguration.getBadgeGracePeriod())
 | 
				
			||||||
            .entity(new SetSubscriptionLevelErrorResponse(List.of(
 | 
					          .truncatedTo(ChronoUnit.DAYS)
 | 
				
			||||||
                new SetSubscriptionLevelErrorResponse.Error(
 | 
					          .plus(1, ChronoUnit.DAYS);
 | 
				
			||||||
                    SetSubscriptionLevelErrorResponse.Error.Type.UNSUPPORTED_LEVEL, null))))
 | 
					      case BACKUP -> paidAt.plus(subscriptionConfiguration.getBackupExpiration())
 | 
				
			||||||
            .build());
 | 
					          .truncatedTo(ChronoUnit.DAYS)
 | 
				
			||||||
      }
 | 
					          .plus(1, ChronoUnit.DAYS);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return Optional.ofNullable(levelConfiguration.getPrices()
 | 
					
 | 
				
			||||||
              .get(currency.toLowerCase(Locale.ROOT)))
 | 
					  private String getSubscriptionTemplateId(long level, String currency, SubscriptionProcessor processor) {
 | 
				
			||||||
          .map(priceConfiguration -> priceConfiguration.processorIds().get(processor))
 | 
					    final SubscriptionLevelConfiguration config = subscriptionConfiguration.getSubscriptionLevel(level);
 | 
				
			||||||
          .orElseThrow(() -> new BadRequestException(Response.status(Status.BAD_REQUEST)
 | 
					    if (config == null) {
 | 
				
			||||||
              .entity(new SetSubscriptionLevelErrorResponse(List.of(
 | 
					      throw new BadRequestException(Response.status(Status.BAD_REQUEST)
 | 
				
			||||||
                  new SetSubscriptionLevelErrorResponse.Error(
 | 
					          .entity(new SetSubscriptionLevelErrorResponse(List.of(
 | 
				
			||||||
                      SetSubscriptionLevelErrorResponse.Error.Type.UNSUPPORTED_CURRENCY, null))))
 | 
					              new SetSubscriptionLevelErrorResponse.Error(
 | 
				
			||||||
              .build()));
 | 
					                  SetSubscriptionLevelErrorResponse.Error.Type.UNSUPPORTED_LEVEL, null))))
 | 
				
			||||||
 | 
					          .build());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    final Optional<String> templateId = Optional
 | 
				
			||||||
 | 
					        .ofNullable(config.prices().get(currency.toLowerCase(Locale.ROOT)))
 | 
				
			||||||
 | 
					        .map(priceConfiguration -> priceConfiguration.processorIds().get(processor));
 | 
				
			||||||
 | 
					    return templateId.orElseThrow(() -> new BadRequestException(Response.status(Status.BAD_REQUEST)
 | 
				
			||||||
 | 
					        .entity(new SetSubscriptionLevelErrorResponse(List.of(
 | 
				
			||||||
 | 
					            new SetSubscriptionLevelErrorResponse.Error(
 | 
				
			||||||
 | 
					                SetSubscriptionLevelErrorResponse.Error.Type.UNSUPPORTED_CURRENCY, null))))
 | 
				
			||||||
 | 
					        .build()));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private SubscriptionManager.Record requireRecordFromGetResult(SubscriptionManager.GetResult getResult) {
 | 
					  private SubscriptionManager.Record requireRecordFromGetResult(SubscriptionManager.GetResult getResult) {
 | 
				
			||||||
    if (getResult == GetResult.PASSWORD_MISMATCH) {
 | 
					    if (getResult == GetResult.PASSWORD_MISMATCH) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -28,6 +28,7 @@ import io.dropwizard.testing.junit5.ResourceExtension;
 | 
				
			||||||
import java.math.BigDecimal;
 | 
					import java.math.BigDecimal;
 | 
				
			||||||
import java.math.RoundingMode;
 | 
					import java.math.RoundingMode;
 | 
				
			||||||
import java.time.Clock;
 | 
					import java.time.Clock;
 | 
				
			||||||
 | 
					import java.time.Duration;
 | 
				
			||||||
import java.time.Instant;
 | 
					import java.time.Instant;
 | 
				
			||||||
import java.util.Arrays;
 | 
					import java.util.Arrays;
 | 
				
			||||||
import java.util.Base64;
 | 
					import java.util.Base64;
 | 
				
			||||||
| 
						 | 
					@ -52,7 +53,15 @@ import org.junit.jupiter.api.Test;
 | 
				
			||||||
import org.junit.jupiter.api.extension.ExtendWith;
 | 
					import org.junit.jupiter.api.extension.ExtendWith;
 | 
				
			||||||
import org.junit.jupiter.params.ParameterizedTest;
 | 
					import org.junit.jupiter.params.ParameterizedTest;
 | 
				
			||||||
import org.junit.jupiter.params.provider.Arguments;
 | 
					import org.junit.jupiter.params.provider.Arguments;
 | 
				
			||||||
 | 
					import org.junit.jupiter.params.provider.CsvSource;
 | 
				
			||||||
import org.junit.jupiter.params.provider.MethodSource;
 | 
					import org.junit.jupiter.params.provider.MethodSource;
 | 
				
			||||||
 | 
					import org.signal.libsignal.zkgroup.InvalidInputException;
 | 
				
			||||||
 | 
					import org.signal.libsignal.zkgroup.ServerSecretParams;
 | 
				
			||||||
 | 
					import org.signal.libsignal.zkgroup.VerificationFailedException;
 | 
				
			||||||
 | 
					import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations;
 | 
				
			||||||
 | 
					import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialRequest;
 | 
				
			||||||
 | 
					import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialResponse;
 | 
				
			||||||
 | 
					import org.signal.libsignal.zkgroup.receipts.ReceiptSerial;
 | 
				
			||||||
import org.signal.libsignal.zkgroup.receipts.ServerZkReceiptOperations;
 | 
					import org.signal.libsignal.zkgroup.receipts.ServerZkReceiptOperations;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
 | 
					import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.badges.BadgeTranslator;
 | 
					import org.whispersystems.textsecuregcm.badges.BadgeTranslator;
 | 
				
			||||||
| 
						 | 
					@ -632,8 +641,14 @@ class SubscriptionControllerTest {
 | 
				
			||||||
    assertThat(response.getStatus()).isEqualTo(409);
 | 
					    assertThat(response.getStatus()).isEqualTo(409);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Test
 | 
					  @ParameterizedTest
 | 
				
			||||||
  void setSubscriptionLevel() {
 | 
					  @CsvSource({
 | 
				
			||||||
 | 
					      "5, M1",
 | 
				
			||||||
 | 
					      "15, M2",
 | 
				
			||||||
 | 
					      "35, M3",
 | 
				
			||||||
 | 
					      "201, M4",
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  void setSubscriptionLevel(long levelId, String expectedProcessorId) {
 | 
				
			||||||
    // set up record
 | 
					    // set up record
 | 
				
			||||||
    final byte[] subscriberUserAndKey = new byte[32];
 | 
					    final byte[] subscriberUserAndKey = new byte[32];
 | 
				
			||||||
    Arrays.fill(subscriberUserAndKey, (byte) 1);
 | 
					    Arrays.fill(subscriberUserAndKey, (byte) 1);
 | 
				
			||||||
| 
						 | 
					@ -662,20 +677,19 @@ class SubscriptionControllerTest {
 | 
				
			||||||
    when(SUBSCRIPTION_MANAGER.subscriptionCreated(any(), any(), any(), anyLong()))
 | 
					    when(SUBSCRIPTION_MANAGER.subscriptionCreated(any(), any(), any(), anyLong()))
 | 
				
			||||||
        .thenReturn(CompletableFuture.completedFuture(null));
 | 
					        .thenReturn(CompletableFuture.completedFuture(null));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final long level = 5;
 | 
					 | 
				
			||||||
    final Response response = RESOURCE_EXTENSION
 | 
					    final Response response = RESOURCE_EXTENSION
 | 
				
			||||||
        .target(String.format("/v1/subscription/%s/level/%d/%s/%s", subscriberId, level, "usd", "abcd"))
 | 
					        .target(String.format("/v1/subscription/%s/level/%d/%s/%s", subscriberId, levelId, "usd", "abcd"))
 | 
				
			||||||
        .request()
 | 
					        .request()
 | 
				
			||||||
        .put(Entity.json(""));
 | 
					        .put(Entity.json(""));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    verify(BRAINTREE_MANAGER).createSubscription(eq(customerId), eq("M1"), eq(level), eq(0L));
 | 
					    verify(BRAINTREE_MANAGER).createSubscription(eq(customerId), eq(expectedProcessorId), eq(levelId), eq(0L));
 | 
				
			||||||
    verifyNoMoreInteractions(BRAINTREE_MANAGER);
 | 
					    verifyNoMoreInteractions(BRAINTREE_MANAGER);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    assertThat(response.getStatus()).isEqualTo(200);
 | 
					    assertThat(response.getStatus()).isEqualTo(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    assertThat(response.readEntity(SubscriptionController.SetSubscriptionLevelSuccessResponse.class))
 | 
					    assertThat(response.readEntity(SubscriptionController.SetSubscriptionLevelSuccessResponse.class))
 | 
				
			||||||
        .extracting(SubscriptionController.SetSubscriptionLevelSuccessResponse::level)
 | 
					        .extracting(SubscriptionController.SetSubscriptionLevelSuccessResponse::level)
 | 
				
			||||||
        .isEqualTo(level);
 | 
					        .isEqualTo(levelId);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @ParameterizedTest
 | 
					  @ParameterizedTest
 | 
				
			||||||
| 
						 | 
					@ -752,10 +766,109 @@ class SubscriptionControllerTest {
 | 
				
			||||||
        Arguments.of("usd", 5, "usd", 5, false),
 | 
					        Arguments.of("usd", 5, "usd", 5, false),
 | 
				
			||||||
        Arguments.of("usd", 5, "jpy", 5, true),
 | 
					        Arguments.of("usd", 5, "jpy", 5, true),
 | 
				
			||||||
        Arguments.of("usd", 5, "usd", 15, true),
 | 
					        Arguments.of("usd", 5, "usd", 15, true),
 | 
				
			||||||
        Arguments.of("usd", 5, "jpy", 15, true)
 | 
					        Arguments.of("usd", 5, "jpy", 15, true),
 | 
				
			||||||
 | 
					        Arguments.of("usd", 201, "usd", 201, false),
 | 
				
			||||||
 | 
					        Arguments.of("usd", 201, "jpy", 201, true)
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Test
 | 
				
			||||||
 | 
					  public void changeSubscriptionLevelInvalid() {
 | 
				
			||||||
 | 
					    // set up record
 | 
				
			||||||
 | 
					    final byte[] subscriberUserAndKey = new byte[32];
 | 
				
			||||||
 | 
					    Arrays.fill(subscriberUserAndKey, (byte) 1);
 | 
				
			||||||
 | 
					    final String subscriberId = Base64.getEncoder().encodeToString(subscriberUserAndKey);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final String customerId = "customer";
 | 
				
			||||||
 | 
					    final String existingSubscriptionId = "existingSubscription";
 | 
				
			||||||
 | 
					    final Map<String, AttributeValue> dynamoItem = Map.of(SubscriptionManager.KEY_PASSWORD, b(new byte[16]),
 | 
				
			||||||
 | 
					        SubscriptionManager.KEY_CREATED_AT, n(Instant.now().getEpochSecond()),
 | 
				
			||||||
 | 
					        SubscriptionManager.KEY_ACCESSED_AT, n(Instant.now().getEpochSecond()),
 | 
				
			||||||
 | 
					        SubscriptionManager.KEY_PROCESSOR_ID_CUSTOMER_ID,
 | 
				
			||||||
 | 
					        b(new ProcessorCustomer(customerId, SubscriptionProcessor.BRAINTREE).toDynamoBytes()),
 | 
				
			||||||
 | 
					        SubscriptionManager.KEY_SUBSCRIPTION_ID, s(existingSubscriptionId));
 | 
				
			||||||
 | 
					    final SubscriptionManager.Record record = SubscriptionManager.Record.from(
 | 
				
			||||||
 | 
					        Arrays.copyOfRange(subscriberUserAndKey, 0, 16), dynamoItem);
 | 
				
			||||||
 | 
					    when(SUBSCRIPTION_MANAGER.create(any(), any(), any(Instant.class)))
 | 
				
			||||||
 | 
					        .thenReturn(CompletableFuture.completedFuture(record));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    when(CLOCK.instant()).thenReturn(Instant.now());
 | 
				
			||||||
 | 
					    when(SUBSCRIPTION_MANAGER.get(any(), any()))
 | 
				
			||||||
 | 
					        .thenReturn(CompletableFuture.completedFuture(SubscriptionManager.GetResult.found(record)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final Object subscriptionObj = new Object();
 | 
				
			||||||
 | 
					    when(BRAINTREE_MANAGER.getSubscription(any()))
 | 
				
			||||||
 | 
					        .thenReturn(CompletableFuture.completedFuture(subscriptionObj));
 | 
				
			||||||
 | 
					    when(BRAINTREE_MANAGER.getLevelAndCurrencyForSubscription(subscriptionObj))
 | 
				
			||||||
 | 
					        .thenReturn(CompletableFuture.completedFuture(
 | 
				
			||||||
 | 
					            new SubscriptionProcessorManager.LevelAndCurrency(201, "usd")));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Try to change from a backup subscription (201) to a donation subscription (5)
 | 
				
			||||||
 | 
					    final Response response = RESOURCE_EXTENSION
 | 
				
			||||||
 | 
					        .target(String.format("/v1/subscription/%s/level/%d/%s/%s", subscriberId, 5, "usd", "abcd"))
 | 
				
			||||||
 | 
					        .request()
 | 
				
			||||||
 | 
					        .put(Entity.json(""));
 | 
				
			||||||
 | 
					    assertThat(response.getStatus()).isEqualTo(400);
 | 
				
			||||||
 | 
					    assertThat(response.readEntity(SubscriptionController.SetSubscriptionLevelErrorResponse.class))
 | 
				
			||||||
 | 
					        .extracting(resp -> resp.errors())
 | 
				
			||||||
 | 
					        .asInstanceOf(InstanceOfAssertFactories.list(SubscriptionController.SetSubscriptionLevelErrorResponse.Error.class))
 | 
				
			||||||
 | 
					        .hasSize(1).first()
 | 
				
			||||||
 | 
					        .extracting(error -> error.type())
 | 
				
			||||||
 | 
					        .isEqualTo(SubscriptionController.SetSubscriptionLevelErrorResponse.Error.Type.UNSUPPORTED_LEVEL);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @ParameterizedTest
 | 
				
			||||||
 | 
					  @CsvSource({"5, P45D", "201, P13D"})
 | 
				
			||||||
 | 
					  public void createReceiptCredential(long level, Duration expectedExpirationWindow)
 | 
				
			||||||
 | 
					      throws InvalidInputException, VerificationFailedException {
 | 
				
			||||||
 | 
					    final byte[] subscriberUserAndKey = new byte[32];
 | 
				
			||||||
 | 
					    Arrays.fill(subscriberUserAndKey, (byte) 1);
 | 
				
			||||||
 | 
					    final String subscriberId = Base64.getEncoder().encodeToString(subscriberUserAndKey);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final String customerId = "customer";
 | 
				
			||||||
 | 
					    final String subscriptionId = "subscriptionId";
 | 
				
			||||||
 | 
					    final Map<String, AttributeValue> dynamoItem = Map.of(SubscriptionManager.KEY_PASSWORD, b(new byte[16]),
 | 
				
			||||||
 | 
					        SubscriptionManager.KEY_CREATED_AT, n(Instant.now().getEpochSecond()),
 | 
				
			||||||
 | 
					        SubscriptionManager.KEY_ACCESSED_AT, n(Instant.now().getEpochSecond()),
 | 
				
			||||||
 | 
					        SubscriptionManager.KEY_PROCESSOR_ID_CUSTOMER_ID,
 | 
				
			||||||
 | 
					        b(new ProcessorCustomer(customerId, SubscriptionProcessor.BRAINTREE).toDynamoBytes()),
 | 
				
			||||||
 | 
					        SubscriptionManager.KEY_SUBSCRIPTION_ID, s(subscriptionId));
 | 
				
			||||||
 | 
					    final SubscriptionManager.Record record = SubscriptionManager.Record.from(
 | 
				
			||||||
 | 
					        Arrays.copyOfRange(subscriberUserAndKey, 0, 16), dynamoItem);
 | 
				
			||||||
 | 
					    final ReceiptCredentialRequest receiptRequest = new ClientZkReceiptOperations(
 | 
				
			||||||
 | 
					        ServerSecretParams.generate().getPublicParams()).createReceiptCredentialRequestContext(
 | 
				
			||||||
 | 
					        new ReceiptSerial(new byte[ReceiptSerial.SIZE])).getRequest();
 | 
				
			||||||
 | 
					    final ReceiptCredentialResponse receiptCredentialResponse = mock(ReceiptCredentialResponse.class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    when(CLOCK.instant()).thenReturn(Instant.now());
 | 
				
			||||||
 | 
					    when(SUBSCRIPTION_MANAGER.get(any(), any()))
 | 
				
			||||||
 | 
					        .thenReturn(CompletableFuture.completedFuture(SubscriptionManager.GetResult.found(record)));
 | 
				
			||||||
 | 
					    when(BRAINTREE_MANAGER.getReceiptItem(subscriptionId)).thenReturn(
 | 
				
			||||||
 | 
					        CompletableFuture.completedFuture(new SubscriptionProcessorManager.ReceiptItem(
 | 
				
			||||||
 | 
					            "itemId",
 | 
				
			||||||
 | 
					            Instant.ofEpochSecond(10).plus(Duration.ofDays(1)),
 | 
				
			||||||
 | 
					            level
 | 
				
			||||||
 | 
					        )));
 | 
				
			||||||
 | 
					    when(ISSUED_RECEIPTS_MANAGER.recordIssuance(eq("itemId"), eq(SubscriptionProcessor.BRAINTREE), eq(receiptRequest), any()))
 | 
				
			||||||
 | 
					        .thenReturn(CompletableFuture.completedFuture(null));
 | 
				
			||||||
 | 
					    when(ZK_OPS.issueReceiptCredential(any(), anyLong(), eq(level))).thenReturn(receiptCredentialResponse);
 | 
				
			||||||
 | 
					    when(receiptCredentialResponse.serialize()).thenReturn(new byte[0]);
 | 
				
			||||||
 | 
					    final Response response = RESOURCE_EXTENSION
 | 
				
			||||||
 | 
					        .target(String.format("/v1/subscription/%s/receipt_credentials", subscriberId))
 | 
				
			||||||
 | 
					        .request()
 | 
				
			||||||
 | 
					        .post(Entity.json(new SubscriptionController.GetReceiptCredentialsRequest(receiptRequest.serialize())));
 | 
				
			||||||
 | 
					    assertThat(response.getStatus()).isEqualTo(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    long expectedExpiration = Instant.EPOCH
 | 
				
			||||||
 | 
					        // Truncated current time is day 1
 | 
				
			||||||
 | 
					        .plus(Duration.ofDays(1))
 | 
				
			||||||
 | 
					        // Expected expiration window
 | 
				
			||||||
 | 
					        .plus(expectedExpirationWindow)
 | 
				
			||||||
 | 
					        // + one day to forgive skew
 | 
				
			||||||
 | 
					        .plus(Duration.ofDays(1)).getEpochSecond();
 | 
				
			||||||
 | 
					    verify(ZK_OPS).issueReceiptCredential(any(), eq(expectedExpiration), eq(level));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Test
 | 
					  @Test
 | 
				
			||||||
  void testGetBankMandate() {
 | 
					  void testGetBankMandate() {
 | 
				
			||||||
    when(BANK_MANDATE_TRANSLATOR.translate(any(), any())).thenReturn("bankMandate");
 | 
					    when(BANK_MANDATE_TRANSLATOR.translate(any(), any())).thenReturn("bankMandate");
 | 
				
			||||||
| 
						 | 
					@ -812,6 +925,7 @@ class SubscriptionControllerTest {
 | 
				
			||||||
                List.of(BigDecimal.valueOf(20))));
 | 
					                List.of(BigDecimal.valueOf(20))));
 | 
				
			||||||
        assertThat(currency.subscription()).isEqualTo(
 | 
					        assertThat(currency.subscription()).isEqualTo(
 | 
				
			||||||
            Map.of("5", BigDecimal.valueOf(5), "15", BigDecimal.valueOf(15), "35", BigDecimal.valueOf(35)));
 | 
					            Map.of("5", BigDecimal.valueOf(5), "15", BigDecimal.valueOf(15), "35", BigDecimal.valueOf(35)));
 | 
				
			||||||
 | 
					        assertThat(currency.backupSubscription()).isEqualTo(Map.of("201", BigDecimal.valueOf(5)));
 | 
				
			||||||
        assertThat(currency.supportedPaymentMethods()).isEqualTo(List.of("CARD", "PAYPAL"));
 | 
					        assertThat(currency.supportedPaymentMethods()).isEqualTo(List.of("CARD", "PAYPAL"));
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -826,6 +940,7 @@ class SubscriptionControllerTest {
 | 
				
			||||||
                List.of(BigDecimal.valueOf(2000))));
 | 
					                List.of(BigDecimal.valueOf(2000))));
 | 
				
			||||||
        assertThat(currency.subscription()).isEqualTo(
 | 
					        assertThat(currency.subscription()).isEqualTo(
 | 
				
			||||||
            Map.of("5", BigDecimal.valueOf(500), "15", BigDecimal.valueOf(1500), "35", BigDecimal.valueOf(3500)));
 | 
					            Map.of("5", BigDecimal.valueOf(500), "15", BigDecimal.valueOf(1500), "35", BigDecimal.valueOf(3500)));
 | 
				
			||||||
 | 
					        assertThat(currency.backupSubscription()).isEqualTo(Map.of("201", BigDecimal.valueOf(500)));
 | 
				
			||||||
        assertThat(currency.supportedPaymentMethods()).isEqualTo(List.of("CARD", "PAYPAL"));
 | 
					        assertThat(currency.supportedPaymentMethods()).isEqualTo(List.of("CARD", "PAYPAL"));
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -840,6 +955,7 @@ class SubscriptionControllerTest {
 | 
				
			||||||
                List.of(BigDecimal.valueOf(20000))));
 | 
					                List.of(BigDecimal.valueOf(20000))));
 | 
				
			||||||
        assertThat(currency.subscription()).isEqualTo(
 | 
					        assertThat(currency.subscription()).isEqualTo(
 | 
				
			||||||
            Map.of("5", BigDecimal.valueOf(5000), "15", BigDecimal.valueOf(15000), "35", BigDecimal.valueOf(35000)));
 | 
					            Map.of("5", BigDecimal.valueOf(5000), "15", BigDecimal.valueOf(15000), "35", BigDecimal.valueOf(35000)));
 | 
				
			||||||
 | 
					        assertThat(currency.backupSubscription()).isEqualTo(Map.of("201", BigDecimal.valueOf(5000)));
 | 
				
			||||||
        assertThat(currency.supportedPaymentMethods()).isEqualTo(List.of("CARD"));
 | 
					        assertThat(currency.supportedPaymentMethods()).isEqualTo(List.of("CARD"));
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -852,7 +968,8 @@ class SubscriptionControllerTest {
 | 
				
			||||||
                    BigDecimal.valueOf(20), BigDecimal.valueOf(30), BigDecimal.valueOf(50), BigDecimal.valueOf(100)), "100",
 | 
					                    BigDecimal.valueOf(20), BigDecimal.valueOf(30), BigDecimal.valueOf(50), BigDecimal.valueOf(100)), "100",
 | 
				
			||||||
                List.of(BigDecimal.valueOf(5))));
 | 
					                List.of(BigDecimal.valueOf(5))));
 | 
				
			||||||
        assertThat(currency.subscription()).isEqualTo(
 | 
					        assertThat(currency.subscription()).isEqualTo(
 | 
				
			||||||
            Map.of("5", BigDecimal.valueOf(5), "15", BigDecimal.valueOf(15),"35", BigDecimal.valueOf(35)));
 | 
					            Map.of("5", BigDecimal.valueOf(5), "15", BigDecimal.valueOf(15), "35", BigDecimal.valueOf(35)));
 | 
				
			||||||
 | 
					        assertThat(currency.backupSubscription()).isEqualTo(Map.of("201", BigDecimal.valueOf(5)));
 | 
				
			||||||
        final List<String> expectedPaymentMethods = List.of("CARD", "SEPA_DEBIT", "IDEAL");
 | 
					        final List<String> expectedPaymentMethods = List.of("CARD", "SEPA_DEBIT", "IDEAL");
 | 
				
			||||||
        assertThat(currency.supportedPaymentMethods()).isEqualTo(expectedPaymentMethods);
 | 
					        assertThat(currency.supportedPaymentMethods()).isEqualTo(expectedPaymentMethods);
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
| 
						 | 
					@ -950,6 +1067,30 @@ class SubscriptionControllerTest {
 | 
				
			||||||
    private static final String SUBSCRIPTION_CONFIG_YAML = """
 | 
					    private static final String SUBSCRIPTION_CONFIG_YAML = """
 | 
				
			||||||
        badgeExpiration: P30D
 | 
					        badgeExpiration: P30D
 | 
				
			||||||
        badgeGracePeriod: P15D
 | 
					        badgeGracePeriod: P15D
 | 
				
			||||||
 | 
					        backupExpiration: P13D
 | 
				
			||||||
 | 
					        backupLevels:
 | 
				
			||||||
 | 
					          201:
 | 
				
			||||||
 | 
					            prices:
 | 
				
			||||||
 | 
					              usd:
 | 
				
			||||||
 | 
					                amount: '5'
 | 
				
			||||||
 | 
					                processorIds:
 | 
				
			||||||
 | 
					                  STRIPE: R4
 | 
				
			||||||
 | 
					                  BRAINTREE: M4
 | 
				
			||||||
 | 
					              jpy:
 | 
				
			||||||
 | 
					                amount: '500'
 | 
				
			||||||
 | 
					                processorIds:
 | 
				
			||||||
 | 
					                  STRIPE: Q4
 | 
				
			||||||
 | 
					                  BRAINTREE: N4
 | 
				
			||||||
 | 
					              bif:
 | 
				
			||||||
 | 
					                amount: '5000'
 | 
				
			||||||
 | 
					                processorIds:
 | 
				
			||||||
 | 
					                  STRIPE: S4
 | 
				
			||||||
 | 
					                  BRAINTREE: O4
 | 
				
			||||||
 | 
					              eur:
 | 
				
			||||||
 | 
					                amount: '5'
 | 
				
			||||||
 | 
					                processorIds:
 | 
				
			||||||
 | 
					                  STRIPE: A4
 | 
				
			||||||
 | 
					                  BRAINTREE: B4
 | 
				
			||||||
        levels:
 | 
					        levels:
 | 
				
			||||||
          5:
 | 
					          5:
 | 
				
			||||||
            badge: B1
 | 
					            badge: B1
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue